C, PHP, VB, .NET

Дневникът на Филип Петров


* XSRF – изпълнение на нежелани заявки

Публикувано на 06 декември 2008 в раздел ОСУП.

Cross Site Request Forgery (XSRF) е уязвимост, с която караме човек да извърши действие в даден сайт без той да желае да извърши това действие. В браузърите действа т.нар. "same origin policy", който гарантира, че в рамките на една сесия на браузъра скрипт от един домейн не може да прочете информация от друг домейн, но то не спира изпращането на информация (GET и POST заявки) от един домейн на друг. Именно това се използва като основа за XSRF атаката. Например XSRF може да се използва в комбинация с XSS, за да бъде накаран потребител с по-високи права да изпълни действие, което не е трябвало да бъде изпълнено, да бъде откраднато сесийно cookie, т.н.
Като един от най-големите осъществени исторически пробиви ще споменем, че почти 18 милиона потребители са загубили лична информация през февруари 2008 от такава уязвимост в корейският eBay (информация от webappsec.org).

Векторите на атака могат да са както през GET, така и през POST. Представете си, че имаме форма на сайта, с която променяме потребителска информация с GET параметър - например в някаква онлайн игра transfer.php?targetacc=10&points=100 да е скрипт, който прехвърля 100 точки от вашия акаунт в акаунт 10. Нека този скрипт се намира на сайта mygame.dom, нека хакерът разполага със сайт thehackersite.dom и нека акаунтът на хакера е с id 666. Тогава при следната постановка:

  1. Вие сте се логнали в акаунта си в mygame.dom;
  2. В съседен таб на браузъра сте посетили нагледно безобидна страничка на сайта thehackersite.dom, която обаче съдържа следния код:
    <img src="http://mygame.dom/transfer?targetacc=666&points=100" alt="some broken image" />

вашият браузър ще се опита да зареди картинката, с което ще извърши прехвърлянето на точките. Естествено хакерът може да направи картинката с ширина и дължина 1 пиксел и да я прикрие така, че да не я забележите изобщо.

Спазването на правилото "GET заявките да са само за четене - ако има промяна на данни, тя трябва да става само през POST" впорчем няма да е особена защита. XSRF атаки могат съвсем лесно да се изпълняват и към пост форми. Ако например прехвърлянето на точки се изпълнява чрез следната форма на сайта mygame.dom:

<form action="transfer.php" method="post">
<input type="text" name="targetacc" />
<input type="text" name="points" />
<input type="submit" name="btnSubmit" value="transfer" />
</form>

то хакерът може да вгради същата форма с предварително попълнени данни на неговия уебсайт и с javascript да ви накара да изпратите съдържанието автоматично:

<form action="http://mygame.dom/transfer.php" method="post">
<input type="hidden" name="targetacc" value="666" />
<input type="hidden" name="points" value="100" />
<input id="submithack" type="submit" name="btnSubmit" value="transfer" />
</form>
<script>
document.getElementById('submithack').click();
</script>

Разбира се и тук хакерът може допълнително с CSS да скрие submit бутона така, че потребителят, който отиде на неговия сайт да не го види. Казано с други думи XSRF атака може да се осъществи на всяко място в сайта, където няма защита и се извършва промяна на данни чрез параметри подадени от потребителя. Забележете, че дали се използва TLS или не няма особено значение. Хакерът извършва атаката без да знае име, парола или да е откраднал cookie.

Нека разгледаме начини за защита от XSRF чрез следната примерна безобидна форма с файл xsrf.php, който изпраща POST заявка сам към себе си:

<html>
<head>
	<title>WorldBank.dom XSRF vulnerable page</title>
</head>
<body>
<?php
	if( isset($_REQUEST['btnSubmit'])){
		echo 'Form is submitted';
	}
	else {
		echo '<form action="xsrf.php" method="POST">';
		echo '<input type="submit" name="btnSubmit">';
		echo '</form><br><br><br>';
	}
?>
</body>
</html>

Нека приемем, че той е качен на адрес mysite.dom/xsrf.php. Когато отворите адреса в браузера и натиснете бутона, то ще се изпълни кода показващ "Form is submitted" на екрана. Искаме да предотвратим възможността за това други домейни да извършат тази POST заявка към тази форма.

Един подход е проверката на "HTTP REFERER" - да видим къде е бил потребителя преди да дойде на нашия сайт. За PHP това става чрез environment променливата, записана като "$_SERVER['HTTP_REFERER']". В нея се записва информацията "откъде е дошла заявката", т.е. кой е адреса на предишната страница, на която сме били. Тоест можем да направим следния опит за защита:

<html>
<head>
	<title>WorldBank.dom XSRF protected page</title>
</head>
<body>
<?php
        // НЕ Е НАДЕЖДНА ЗАЩИТА
	if( isset($_REQUEST['btnSubmit'])){
		if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER']!="http://mysite.dom/xsrf.php"){
			echo 'You are referred from a different site</body></html>';
			return;
		}
		else{
			echo 'Form is submitted';
		}
	}
	else{
		echo '<form action="xsrf.php" method="POST">';
		$_SESSION['token'] = uniqid(rand(), true);
		echo '<input type="hidden" name="token" value="'.$_SESSION['token'].'">';
		echo '<input type="submit" name="btnSubmit">';
		echo '</form><br><br><br>';
	}
?>
</body></html>

Имайте предвид, че сравнението по HTTP_REFERER header може да пропадне при някои proxy сървъри или ако клиента има строги настройки на своята защитна стена или антивирусна програма. Освен това този хедър НЕ се изпраща от браузърите когато има преминаване от HTTPS към HTTP канал - това трябва да се има предвид, защото потенциално води до принуда на браузъра да не изпраща хедъра. Не на последно място има редица трикове, с които хакера може да се опита да накара браузъра на клиента да НЕ изпрати HTTP_REFERER хедър, например: <iframe src="javascript:..."></iframe> - в Javascript кода се генерира форма и се прави автоматично submit.

Казано с други думи на HTTP_REFERER НЕ трябва да се разчита. С цел да се разреши този проблем след 2011 г. за повечето браузъри се въвежда един нов хедър, който се нарича HTTP_ORIGIN (*). Идеята при него е същата както при HTTP_REFERER - да покаже сайта, от който идваме, - но с тази разлика, че не съдържа REQUEST_URI, а само домейна. Повечето браузъри изпращат този хедър дори през HTTPS, което разрешава основния проблем на HTTP_REFERER. Казано с други думи, можем да пренапишем защитата по следния начин:

<html>
<head>
	<title>WorldBank.dom XSRF protected page</title>
</head>
<body>
<?php
        // ПРОДЪЛЖАВА ДА НЕ Е НАДЕЖДНА ЗАЩИТА
	if( isset($_REQUEST['btnSubmit'])){
		if (isset($_SERVER['HTTP_ORIGIN']) && $_SERVER['HTTP_ORIGIN']!="http://mysite.dom"){
			echo 'You are referred from a different site</body></html>';
			return;
		}
                else if(isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER']!="http://mysite.dom/xsrf.php"){
                        echo 'You are reffered from a different site</body></html>';
                        return;
		else{
			echo 'Form is submitted';
		}
	}
	else{
		echo '<form action="xsrf.php" method="POST">';
		$_SESSION['token'] = uniqid(rand(), true);
		echo '<input type="hidden" name="token" value="'.$_SESSION['token'].'">';
		echo '<input type="submit" name="btnSubmit">';
		echo '</form><br><br><br>';
	}
?>
</body></html>

Въпреки това нововъведение, тази защита продължава да не е надеждна. Съществуват методи с които хакера може да заблуди браузъра да не изпрати тези хедъри. Firefox пък спря да го изпраща по подразбиране за много дълго време заради бъг. В Opera, IE и Edge също не е включено по подразбиране. Тоест горната проверка (към април 2019 г.) ще работи само с Google Chrome (по-късно поддръжката се върна). Все пак бихме могли да "затегнем режима", но с долния код сайтът ни потенциално ще спре да работи под някои браузъри:

<html>
<head>
	<title>WorldBank.dom XSRF protected page</title>
</head>
<body>
<?php
        // НЕ РАБОТИ С МНОГО БРАУЗЪРИ!
	if( isset($_REQUEST['btnSubmit'])){
		if (!isset($_SERVER['HTTP_ORIGIN']) || $_SERVER['HTTP_ORIGIN']!="http://mysite.dom"){
			echo 'You are referred from a different site</body></html>';
			return;
		}
                else if(isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER']!="http://mysite.dom/xsrf.php"){
                        echo 'You are reffered from a different site</body></html>';
                        return;
		else{
			echo 'Form is submitted';
		}
	}
	else{
		echo '<form action="xsrf.php" method="POST">';
		$_SESSION['token'] = uniqid(rand(), true);
		echo '<input type="hidden" name="token" value="'.$_SESSION['token'].'">';
		echo '<input type="submit" name="btnSubmit">';
		echo '</form><br><br><br>';
	}
?>
</body></html>

Именно несъвместимостта със стари браузъри (и евентуално лошо написани plugins за браузъри) е основната спирачка за имплементирането на тази доста лесна защита срещу XSRF. Затова ще покажем и друг вариант чрез използване на потребителска сесия и записване на произволен token в нея. Като скрито поле вътре във формата трябва да запишем произволно генерирано число. Това число се записва и в потребителската сесия . Когато скрипта получи заявка, той трябва да сравни числото предадено чрез $_POST и да го сравни с това, записано в $_SESSION. Ето как бихме реализирали това с горния скрипт:

<?php
	function security_start_session($ssl, $timeout, $maxtime, $ip){
		// ... security checks
		session_start();
		session_regenerate_id(true);
		return 1;
	}
	if (security_start_session(0, 6*60, 20*60, 1) != 1){
		echo "<br>session destroyed!";
		return;
	}
?>
<html>
<head>
	<title>WorldBank.dom XSRF protected page</title>
</head>
<body>
<?php
        // Надеждна защина, но с проблеми с нови табове
	if(isset($_REQUEST['btnSubmit'])){
		if (!isset($_SESSION['token']) 
                    || !isset($_POST['token']) 
                    || !is_string($_POST['token'])
                    || $_SESSION['token']!=$_POST['token']){
			session_unset();
			session_destroy();
			echo "No XSRF token in your query</body></html>";
			return;
		}
		else{
			echo 'Form is submitted';
		}
	}
	else{
		echo '<form action="xsrf.php" method="POST">';
		$_SESSION['token'] = uniqid(rand(), true);
		echo '<input type="hidden" name="token" value="'.$_SESSION['token'].'">';
		echo '<input type="submit" name="btnSubmit">';
		echo '</form><br><br><br>';
	}
?>
</body></html>

Тук обезателно се налага стартирането на потребителска сесия (което тъй или иначе се прави в повечето случаи при динамични сайтове). Алтернативно може да запишете и browser cookie. Обърнете внимание на удебеления текст, където се генерира случайният token. Чрез него сме сигурни, че формата е попълнена именно от нашият сървър. Тук използваме same origin policy, който не позволява на домейна на хакера да се сдобие с въпросния xsrf token (напомняме, че един домейн не може да чете информация, която е визуализирана в друг домейн).

Тази защита е "bullet proof", т.е. при нея няма отказ и е съвместима с всякакви браузъри, но за съжаление продължава да има проблеми - ако потребителя отвори същата страница в нов таб или нов прозорец на браузъра, в стария таб ще остане стар token в сесията. Тоест ако потребителя се върне в стария таб и натисне бутона, ще бъде засечена XSRF атака.

Разрешаването на този проблем може да се атакува по един от следните три начина:

  1. XSRF token да се генерира еднократно за цялата сесия. Това например може да бъде при първоначалното стартиране на сесията по време на извършване на login. Така този token няма да се сменя при различните форми. Този метод има потенциален проблем с MITM при извършване на BREACH атака при наличие на HTTP компресия;
  2. Да пазим стари XSRF tokens в масив и да ги премахваме когато бъдат употребени. Идеята е освен различен token за всяка страница да генерираме и id (може да е произволно или поредно) на този token и да ги записваме веднъж в сесията като id се използва за индекс на масива, а token е стойност, и втори път в POST. След това сравнението става лесно, а след като са сравнени успешно, изтриваме вече употребения token, за да освободим място.  По принцип това ще доведе до увеличен обем на файловете с потребителските сесии, защото потребителите няма винаги да попълнят форма на всяка страница, на която попаднат;
  3. Недостатък в 2. е, че пълним сесийния файл с много tokens, което е безсмислено разхищение на информация и дискови операции. Алтернатива е да се генерира статичен XSRFToken при логването на потребителя (остава един и същи за всяка страница в сайта) и втори, който е уникален за всяко отделно зареждане на страница - XSRFTokenMASK. При зареждането на страницата "разбъркваме" статичния token с маската - например по следния начин - hash("sha256", hex2bin($xsrftoken)^hex2bin($xsrftokenMASK)) - и го изпращаме в този вид заедно с маската към следващата страница. При получаване на заявката се взима маската, взима се оригиналния статичен XSRFToken, "разбърква" се с маската и се сравнява с подадената вече "разбъркана" комбинация. Още по-добър вариант (спрямо простия XOR) е статичния да се използва като ключ за подписване на маската - например с използване на hash_hmac.

Важно е да се отбележи също, че няма проблем на една страница да използвате един и същи token за различни форми.

Задача 1. Реализирайте втория метод (с масив от tokens) и третия метод (препоръчителен!).

Задача 2. Реализирайте комбинация от двата метода - проверка на HTTP_ORIGIN и XSRF tokens.

(*) 29.04.2024 г. Към днешна дата HTTP_ORIGIN отново е отхвърлен от браузърите и вече не се използва. Вместо това съществува header HTTP_SEC_FETCH_SITE. Когато го прочетете, ще се върне следния резултат:

  • same-origin - заявката е изпратена от същия домейн (също протокол и порт разбира се)
  • same-site - заявката е от същия домейн или поддомейн
  • cross-site - заявката идва от чужд домейн
  • none - потребителят е отишъл директно на адреса, т.е. не е попълвал форма

Така бихме могли да направим защитата по следния начин:

if(isset($_SERVER['HTTP_SEC_FETCH_SITE']) && !in_array($_SERVER['HTTP_SEC_FETCH_SITE'], ["same-origin", "same-site", "none"])){
   echo "XSRF";
   exit;
}

 



2 коментара


  1. Относно задачата:
    Ще разгледам 2 варианта.
    - Проверка на Mime Type на дадения файл. Предполагам, че може да се излъжe обаче.
    - Ако снимката, ще се качва, предполагам, че ще има и умален размер. Ако не е валидна снимка, със сигурност ще даде грешка при оразмеряване и другите необходими функции за направа на thumb, и съответно няма да има качена снимка на сървъра в thumb размер. Съответно след проверка ще изпише, че снимката не е валидна. Това точи доста ресурс за съжаление. Някакви варианти да предложиш ти?

  2. Пуснал си коментара в грешна статия. Мисля, че най-подходяща е функцията "getimagesize()". А mime type се проверява задължително при всички положения!

Добави коментар

Адресът на електронната поща няма да се публикува


*