XSS: чужд скрипт в твоята страница
Проблем
Търсене, което краде
Магазинът има търсачка. Страницата с резултатите показва какво е написал купувачът: „Резултати за: маратонки“. Сървърът слепва текста от търсенето в HTML:
using System.Net;
string SearchPage(string query){ return "<p>Резултати за: <b>" + query + "</b></p>";}
Console.WriteLine(SearchPage("маратонки"));Console.WriteLine(SearchPage("<img src=x onerror=\"steal(shopSession)\">"));<p>Резултати за: <b>маратонки</b></p><p>Резултати за: <b><img src=x onerror="steal(shopSession)"></b></p>Първият ред е обикновена страница. Във втория търсенето е <img src=x onerror="steal(shopSession)">, и то е стигнало до HTML-а цяло. Браузърът вижда таг за картинка, картинката x не се зарежда, и той пуска кода в onerror. Тук steal е измислена функция; в истинска атака кодът праща бисквитките или сесията на сървъра на атакуващия.
Атакуващият не пише в своята търсачка. Той праща на купувачите връзка към търсене в магазина с този текст в адреса. Който я отвори, пуска чуждия код в магазина, със своята сесия. Това е XSS (cross-site scripting): чужд скрипт в твоята страница.
Идеята
HTML е още един език
Това е инжекцията от предишния урок, в още един език. В HTML < започва таг, а " затваря стойност на атрибут. Кодът слепи текста на купувача в езика на браузъра, и знаците в текста станаха част от страницата.
Два вида се срещат най-често:
- Отразен (reflected): текстът идва от адреса и се връща в отговора, като търсенето по-горе. Атакуващият трябва да накара някого да отвори връзката.
- Запазен (stored): текстът е записан в базата, например отзив, и се показва на всеки, който отвори продукта. Атакуващият пише веднъж, а скриптът тръгва при всички.
Сървърът не е изпълнил нищо. Кодът тръгва в браузъра на купувача, с неговата сесия, като че ли е част от магазина.
Виж го
Пробвай сам
Напиши текст в търсачката на магазина, избери защита и виж какво става. Страницата е в изолиран iframe, без мрежа, и не вижда нищо от този сайт.
Пясъчник: злонамерен вход в страница на магазина
Страницата (в изолиран iframe, без мрежа)
Какво се случи
Какво стига до браузъра
Пясъчникът е изолиран: няма мрежа и не вижда нищо от сайта. steal() тук е мнимият сървър на атакуващия; истинската атака праща данните с fetch.
Пробвай трите готови входа без защита, после с escape, после с CSP. Виж в „Какво стига до браузъра“ как изглежда HTML-ът в трите случая.
Идеята
Escape на изхода
Поправката е escape: преди да сложиш текст в HTML, сменяш петте знака, които HTML тълкува, с техните същности (entities). Браузърът показва същността като знак, но не я чете като част от страницата:
| Знак | Става | Защо |
|---|---|---|
& |
& |
започва същност; сменя се първи |
< |
< |
започва таг |
> |
> |
затваря таг |
" |
" |
затваря стойност на атрибут |
' |
' |
затваря стойност в единични кавички |
WebUtility.HtmlEncode от System.Net сменя същите пет знака.using System.Net;
string SearchPage(string query){ return "<p>Резултати за: <b>" + WebUtility.HtmlEncode(query) + "</b></p>";}
Console.WriteLine(SearchPage("маратонки"));Console.WriteLine(SearchPage("<img src=x onerror=\"steal(shopSession)\">"));<p>Резултати за: <b>маратонки</b></p><p>Резултати за: <b><img src=x onerror="steal(shopSession)"></b></p>Сега търсенето стигна до браузъра като текст. Купувачът вижда точно това, което е написал, но таг няма.
Правилото е escape на изхода: при показване, на мястото, където текстът влиза в HTML. Не при приемане: тогава не знаеш къде ще отиде текстът (в HTML, в JSON, в имейл), и в базата остават < вместо това, което купувачът е написал.
По-точно, за любопитните
-
В истински проект HTML-ът се прави с Razor, а той прави escape сам:
@Model.Textе безопасно.@Html.Raw(…)изключва escape и е мястото, което се проверява при преглед на кода. - Escape зависи от мястото. Петте знака стигат за текст между тагове и за стойност на атрибут в кавички. Атрибут без кавички, текст вътре в
<script>, стил и адрес вhrefимат други правила. Най-простото: слагай текст отвън само между тагове и в атрибути с кавички. - Адресът в
href.javascript:steal(shopSession)няма нито един от петте знака, а пак пуска код при клик. За адреси от потребителя трябва allow-list, както в урока „Недоверен вход“: адресът започва сhttps://илиhttp://, иначе не става връзка.
Пробвай
Намери полето
Страницата с отзиви показва заглавие, автор и текст. Кое поле стига до браузъра сурово?
Избери реда с грешката.
Последното: текстът на отзива. Заглавието и авторът минават през WebUtility.HtmlEncode, а text е слепен както е. Това е запазен XSS: един отзив, и скриптът тръгва при всеки, който отвори продукта.
Пробвай
Какво ще отпечата
Какво ще отпечата escape за Tom & Jerry <3?
using System.Net;
Console.WriteLine(WebUtility.HtmlEncode("Tom & Jerry <3"));& става &, а < става <. Вторият отговор е забравил &. Третият е escape, който сменя & последен: тогава сменя и & в <, който току-що е сложил, и купувачът вижда на страницата <3 вместо <3.
Идеята
CSP: втората линия
В голям сайт има стотици места, където текст влиза в HTML. Достатъчно е да пропуснеш едно. Затова има втора защита: Content-Security-Policy (CSP). Това е заглавка в отговора, която казва на браузъра кои скриптове може да тръгнат:
Content-Security-Policy: script-src 'self'; object-src 'none'; base-uri 'none'script-src 'self' значи: само скриптове от файлове на същия сайт. Скрипт, написан вътре в страницата (<script>…</script> или onerror="…"), не тръгва, а браузърът записва нарушението. Затова и собственият JavaScript на сайта трябва да е във файлове. Ако все пак ти трябва скрипт в страницата, CSP приема такъв с nonce: случайна стойност, нова при всеки отговор, която атакуващият не знае. Така работи защитата „CSP“ в пясъчника.
CSP не замества escape. Тя спира скрипта, но не и чужд HTML: фалшив формуляр „Влез отново“ или подменена цена не са скрипт. Escape е първата линия, CSP е мрежата под нея.
По-точно, за любопитните
- Пробвай без да чупиш.
Content-Security-Policy-Report-Onlyсамо съобщава какво би спряла политиката. Така се въвежда CSP в стар сайт: първо се гледат нарушенията, после се включва. - Бисквитката на сесията. Дори скриптът да тръгне, бисквитка с флаг
HttpOnlyне се вижда от JavaScript. Това идва в урока за сесиите.
Упражнение
Отзивът
Реши задачата в редактора си.
Отзив с автор. Напиши RenderReview(author, text). Тя връща <article><h3>автор</h3><p>текст</p></article>, като и авторът, и текстът минават през escape. Пробвай с обикновен отзив и с автор, който се казва <script>steal(shopSession)</script>:
<article><h3>Ани</h3><p>Удобни са <3</p></article><article><h3><script>steal(shopSession)</script></h3><p>Супер!</p></article>Още задачи
- Връзка в отзива. Добави на отзива поле „сайт на автора“. Покажи го като
<a href="…">само ако адресът започва сhttps://илиhttp://; иначе го покажи като обикновен текст. Пробвай сjavascript:steal(shopSession). - Двоен escape. Какво ще види купувачът, ако отзивът мине през escape веднъж при записа в базата и още веднъж при показването? Пробвай с
<3.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Методи · Параметри и return · Недоверен вход · Инжекции: когато данните станат команда
- Задачи по темата: Сигурност за програмисти