Translate: 
EnglishFrenchGermanItalianPolishPortugueseRussianSpanish

Oszukać przeznaczenie, czyli łamanie SAFE_MODE oraz max_execution_time

Większość programistów PHP choć raz w swoim życiu spotkało się z takimi terminami jak SAFE_MODE oraz limitem czasowym wykonywania programu w PHP (domyślnie 30 sekund).

O ile obie te rzeczy można łatwo wyłączyć, gdy pracuje się na serwerze dedykowanym lub w środowisku wewnętrznym firmy, to na hostingach współdzielonych obie te rzeczy potrafią przyprawić o ból głowy, wyłączając skrypt (np. migrujący dużą ilość danych) w trakcie jego wykonywania.

W tym artykule przedstawię prostą metodę na obejście tego zabezpieczenia.

O limitach

Głównym założeniem limitu czasowego jest to, że wygenerowanie prostej strony internetowej trwa zazwyczaj ułamki sekund, czasy powyżej 30 sekund uważane są więc za potencjalny błąd w aplikacji (np. wpadnięcie w nieskończoną pętlę). Dlatego też w celu oszczędzenia mocy CPU skrypty działające zbyt długo przerywane są pojawieniem się błędu typu E_FATAL.

Oczywiście język PHP udostępnia programistom mechanizm wyłączenia tych limitów (poprzez wywołanie funkcji set_time_limit(0)), lecz na shared hostingach funkcja ta jest domyślnie wyłączona, lub jej ustawienia ignorowane (włączony tryb SAFE_MODE nie pozwala na zmianę limitu). Dlatego też skupimy się na mało znanej właściwości języka PHP….

Praktyka

Niewielu programistów zdaje sobie sprawę, że w języku PHP wystąpienie błędu fatalnego nie oznacza w rzeczywistości końca wykonywania skryptu, po takim błędzie wykonywany jest jeszcze szereg innych funkcji użytkownika przekazanych jako callback’i do dwóch popularnych poleceń: ob_start($funkcja), oraz register_shutdown_function($funkcja).

Oto przyładowy kod operujący na register_shutdown_function():

<?php
 
// zmniejszam limit z 30 sekund do 3, by zobaczyc szybko efekty
// w przypadku SAFE_MODE ponizsza funkcja nie zadziala
set_time_limit(3);
ignore_user_abort(1);
 
// wyrzucam bledy na ekran
ini_set('display_errors', true);
error_reporting(E_ALL);
date_default_timezone_set('Europe/Warsaw');
 
register_shutdown_function('onExit');
 
echo "<pre>";
 
// inicjuje licznik czasu
getTimeout();
 
// probuje przekroczyc limit czasu
$i = 0;
do {
   $i++;	
}  while(true);
 
// moja funkcja pomocnicza
function getTimeout() {
	static $myTime = 0;
	$time = time();
	if(!$myTime) {
		$myTime = $time;
	}
	$timeout = $time - $myTime;
	$myTime = $time;
	return "[".date('H:i:s')."] !did nothing for additional $timeout seconds!<br>";
}
 
// handler obslugi wyjscia z PHP
function onExit() {
	echo '<br><strong>'.__FUNCTION__.'(): '.getTimeout()."</strong>";
	file_put_contents('debug.txt', 'onExit(): '.getTimeout()."\r\n", FILE_APPEND);
	$time = time();
	// ponownie probuje przekroczyc limit czasu
	$i = 0;
	do {
		$i++;
	} while(true);
}

W efekcie wykonania tego programu otrzymujemy następujący wynik:

Fatal error: Maximum execution time of 3 seconds exceeded in C:\lotos\public_html\example.php on line 24
 
onExit(): [13:49:01] !did nothing for additional 3 seconds!
 
Fatal error: Maximum execution time of 3 seconds exceeded in C:\lotos\public_html\example.php on line 48

Jak widać przekroczyliśmy limit dwukrotnie. To w przypadku SAFE_MODE daje nam łącznie 60 sekund działania kodu!

Czy można jeszcze więcej?

To nas nadal nie satysfakcjonuje, w końcu chcemy by nasza aplikacja działała tak długo jak MY tego chcemy, a nie administrator serwera. Posłużmy się więc drugą ze wspomnianych funkcji PHP, czyli ob_start().

Oto rozbudowana wersja poprzedniego kodu:

<?php
 
// zmniejszam limit z 30 sekund do 3, by zobaczyc szybko efekty
// w przypadku SAFE_MODE ponizsza funkcja nie zadziala
set_time_limit(3);
ignore_user_abort(1);
 
// wyrzucam bledy na ekran
ini_set('display_errors', true);
error_reporting(E_ALL);
date_default_timezone_set('Europe/Warsaw');
 
register_shutdown_function('onExit');
ob_start('onFlush');
 
echo "<pre>";
 
// inicjuje licznik czasu
getTimeout();
 
// probuje przekroczyc limit czasu
$i = 0;
do {
   $i++;	
}  while(true);
 
// moja funkcja pomocnicza
function getTimeout() {
	static $myTime = 0;
	$time = time();
	if(!$myTime) {
		$myTime = $time;
	}
	$timeout = $time - $myTime;
	$myTime = $time;
	return "[".date('H:i:s')."] !did nothing for additional $timeout seconds!<br>";
}
 
// handler obslugi wyjscia z PHP
function onExit() {
	echo '<br><strong>'.__FUNCTION__.'(): '.getTimeout()."</strong>";
	file_put_contents('debug.txt', 'onExit(): '.getTimeout()."\r\n", FILE_APPEND);
	$time = time();
	// ponownie probuje przekroczyc limit czasu
	$i = 0;
	do {
		$i++;
	} while(true);
}
 
function onFlush($buffer) {
	$timeout = getTimeout();
	file_put_contents('debug.txt', 'onFlush(): '.$timeout."\r\n", FILE_APPEND);
	$buffer .= 'onFlush(): '.$timeout."\r\n";
 
	$time = time();
	$i = 0;
	do {
		$i++;
	} while(time() < $time + 2);
	$timeout = getTimeout();
	file_put_contents('debug.txt', 'onFlush(): '.$timeout."\r\n", FILE_APPEND);
	$buffer .= 'onFlush(): '.$timeout."\r\n";
	return $buffer;
}

Oraz otrzymany wynik:

Fatal error: Maximum execution time of 3 seconds exceeded in C:\lotos\public_html\example.php on line 24
 
onExit(): [13:49:01] !did nothing for additional 3 seconds!
 
Fatal error: Maximum execution time of 3 seconds exceeded in C:\lotos\public_html\example.php on line 48
onFlush(): [13:49:04] !did nothing for additional 3 seconds!
 
onFlush(): [13:49:06] !did nothing for additional 2 seconds!

Już lepiej, mamy dodatkowe 30 sekund w przypadku SAFE_MODE (łącznie więc 90 sekund zamiast 30). Ale to nadal daleko od nieskończoności…

Chcemy braku ograniczeń

W tym celu jednak musimy wykonać dodatkowe voodoo magic. Niestety ilość funkcji opóźniających przerwanie skryptu w PHP jest skończona, a nie możemy ich wykonywać ani ponownie, ani zagnieżdzać (np. bufory). Musimy więc w końcu zrezygnować z danego procesu PHP i w ramach ostatniej dostępnej funkcji… uciec do nowego procesu!

Teraz przyda się znajomość funkcji serialize() i unserialize() oraz kilkanaście linijek kodu wysyłającego dane umierającego już procesu PHP do nowego poprzez np. protokół HTTP metodą POST. To jednak pozostawiam jako pracę domową.

Jak się przed tym zabezpieczyć?

Jak widać blokady wbudowane w język PHP są łatwe do obejścia, jedyny sposób by temu zaradzić od strony administratora serwera to nałożyć na użytkownika WWW limity systemowe, np poleceniem ulimit -t (sprawdza się bardzo dobrze w przypadku procesów FASTCGI, lecz po skillowaniu procesu generuje błąd nr 500).

Tagi:

Dodaj odpowiedź