Latte е синоним на сигурност
Latte е единствената система за шаблони за PHP с ефективна защита срещу критичната уязвимост Cross-site Scripting (XSS). И това е благодарение на т.нар. контекстно-чувствително екраниране. Ще си поговорим за:
- какъв е принципът на уязвимостта XSS и защо е толкова опасна
- защо Latte е толкова ефективен в защитата срещу XSS
- как в шаблоните на Twig, Blade и други подобни може лесно да се направи дупка в сигурността
Cross-site Scripting (XSS)
Cross-site Scripting (съкратено XSS) е една от най-често срещаните уязвимости на уеб страниците и същевременно много опасна. Тя позволява на нападателя да вмъкне в чужда страница зловреден скрипт (т.нар. malware), който се стартира в браузъра на нищо неподозиращия потребител.
Какво всичко може да направи такъв скрипт? Може например да изпрати на нападателя всякакво съдържание от нападнатата страница, включително чувствителни данни, показани след влизане. Може да промени страницата или да извършва други заявки от името на потребителя. Ако например става въпрос за уебмейл, може да прочете чувствителни съобщения, да промени показваното съдържание или да пренастрои конфигурацията, напр. да включи препращане на копия на всички съобщения към адреса на нападателя, за да получи достъп и до бъдещи имейли.
Затова XSS фигурира на водещи места в класациите на най-опасните уязвимости. Ако на уеб страница се появи уязвимост, е необходимо тя да бъде отстранена възможно най-скоро, за да се предотврати злоупотреба.
Как възниква уязвимостта?
Грешката възниква на мястото, където се генерира уеб страницата и се извеждат променливи. Представете си, че създавате страница с търсене, и в началото ще има параграф с търсения израз във вида:
echo '<p>Резултати от търсенето за <em>' . $search . '</em></p>';
Нападателят може в полето за търсене и съответно в променливата
$search
да запише произволен низ, т.е. и HTML код като
<script>alert("Hacked!")</script>
. Тъй като изходът не е обработен по
никакъв начин, той става част от показаната страница:
<p>Резултати от търсенето за <em><script>alert("Hacked!")</script></em></p>
Браузърът, вместо да изпише търсения низ, стартира JavaScript. И така нападателят поема контрола над страницата.
Можете да възразите, че вмъкването на код в променлива наистина ще доведе до стартиране на JavaScript, но само в браузъра на нападателя. Как ще стигне до жертвата? От тази гледна точка разграничаваме няколко типа XSS. В нашия пример с търсенето говорим за reflected XSS. Тук е необходимо още да се насочи жертвата да кликне върху връзка, която ще съдържа зловреден код в параметъра:
https://example.com/?search=<script>alert("Hacked!")</script>
Насочването на потребителя към връзката наистина изисква известно
социално инженерство, но не е нищо сложно. Потребителите кликват върху
връзки, било то в имейли или в социалните мрежи, без много да мислят. А
това, че в адреса има нещо подозрително, може да се маскира с помощта на
съкратител на URL, потребителят тогава вижда само bit.ly/xxx
.
Въпреки това съществува и втора, много по-опасна форма на атака, наречена stored XSS или persistent XSS, при която нападателят успява да съхрани зловреден код на сървъра така, че той автоматично да се вмъква в някои страници.
Пример за това са страниците, където потребителите пишат коментари. Нападателят изпраща публикация, съдържаща код, и той се съхранява на сървъра. Ако страниците не са достатъчно защитени, той ще се стартира в браузъра на всеки посетител.
Може да изглежда, че ядрото на атаката се състои в това да се вкара в
страницата низът <script>
. В действителност начините за вмъкване
на JavaScript са много. Ще покажем например пример за вмъкване с помощта
на HTML атрибут. Нека имаме фотогалерия, където към изображенията може да
се добавя описание, което се изписва в атрибута alt
:
echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';
На нападателя е достатъчно като описание да вмъкне умело съставен
низ " onload="alert('Hacked!')
и ако изписването не е обработено,
резултатният код ще изглежда така:
<img src="photo0145.webp" alt="" onload="alert('Hacked!')">
Част от страницата сега става подправеният атрибут onload
.
Браузърът ще стартира кода, съдържащ се в него, веднага след
изтеглянето на изображението. Hacked!
Как да се защитим от XSS?
Всякакви опити за откриване на атака с помощта на черен списък, като
например блокиране на низа <script>
и др., са недостатъчни.
Основата на функционалната защита е последователната санитация на
всички данни, извеждани вътре в страницата.
Преди всичко става въпрос за замяна на всички знаци със специално
значение с други съответстващи последователности, което разговорно се
нарича екраниране (първият знак на последователността се нарича
екраниращ, оттук и името). Например в HTML текст специално значение има
знакът <
, който, ако не трябва да бъде интерпретиран като
начало на таг, трябва да го заменим с визуално съответстваща
последователност, т.нар. HTML ентичност <
. И браузърът ще
изпише знак за по-малко.
Много е важно да се разграничава контекстът, в който извеждаме данните. Защото в различни контексти низовете се санират по различен начин. В различни контексти специално значение имат различни знаци. Например екранирането в HTML текст се различава от това в HTML атрибути, вътре в някои специални елементи и т.н. След малко ще го разгледаме подробно.
Обработката е най-добре да се извършва директно при изписването на низа в страницата, с което се гарантира, че наистина ще се извърши и ще се извърши точно веднъж. Най-добре е, ако обработката се осигурява автоматично директно от системата за шаблони. Защото ако обработката не се извършва автоматично, програмистът може да я забрави. А едно пропускане означава, че уебсайтът е уязвим.
Въпреки това XSS не се отнася само до извеждането на данни в шаблони, но
и до други части на приложението, които трябва правилно да боравят с
ненадеждни данни. Например е необходимо JavaScript във вашето приложение да
не използва във връзка с тях innerHTML
, а само innerText
или
textContent
. Специално внимание трябва да се обръща на функциите,
които оценяват низове като JavaScript, което е eval()
, но също и
setTimeout()
, или използването на функцията setAttribute()
с атрибути
за събития като onload
и др. Това обаче вече излиза извън областта,
която покриват шаблоните.
Идеалната защита в 3 точки:
- разпознава контекста, в който се извеждат данните
- санира данните според правилата на дадения контекст (т.е. „контекстно-чувствително“)
- прави го автоматично
Контекстно-чувствително екраниране
Какво точно се разбира под думата контекст? Това е място в документа със собствени правила за обработка на извежданите данни. Зависи от типа на документа (HTML, XML, CSS, JavaScript, plain text, …) и може да се различава в конкретните му части. Например в HTML документ има цяла редица такива места (контексти), където важат много различни правила. Може би ще се изненадате колко са. Ето първите четири:
<p>#text</p>
<img src="#atribut">
<textarea>#rawtext</textarea>
<!-- #komentář -->
Изходният и основен контекст на HTML страницата е HTML текстът. Какви
правила важат тук? Специално значение имат знаците <
и
&
, които представляват начало на таг или ентичност, така че
трябва да ги екранираме, като ги заменим с HTML ентичност (<
с
<
&
с &
).
Вторият най-често срещан контекст е стойността на HTML атрибут.
Различава се от текста по това, че специално значение тук има кавичката
"
или '
, която огражда атрибута. Тя трябва да се запише
като ентичност, за да не бъде разбирана като край на атрибута. Обратно,
в атрибута може безопасно да се използва знакът <
, защото тук
няма никакво специално значение, тук не може да бъде разбиран като
начало на таг или коментар. Но внимавайте, в HTML може да се пишат
стойности на атрибути и без кавички, в такъв случай специално значение
има цяла редица знаци, следователно става въпрос за друг самостоятелен
контекст.
Може би ще ви изненада, но специални правила важат вътре в елементите
<textarea>
и <title>
, където знакът <
не е
необходимо (но може) да се екранира, ако след него не следва /
. Но
това е по-скоро любопитен факт.
Интересно е вътре в HTML коментарите. Тук за екраниране не се използват HTML ентичности. Дори никоя спецификация не посочва как трябва да се екранира в коментарите. Само е необходимо да се спазват донякъде любопитни правила и да се избягват в тях определени комбинации от знаци.
Контекстите също могат да се наслояват, което се случва, когато вмъкнем JavaScript или CSS в HTML. Това може да се направи по два различни начина, с елемент и с атрибут:
<script>#js-element</script>
<img onclick="#js-atribut">
<style>#css-element</style>
<p style="#css-atribut"></p>
Два пътя и два различни начина за екраниране на данни. Вътре в
елементите <script>
и <style>
точно както в случая с HTML
коментарите, екранирането с помощта на HTML ентичности не се извършва.
При извеждане на данни вътре в тези елементи е необходимо да се спазва
единственото правило: текстът не трябва да съдържа последователността
</script
съответно </style
.
Обратно, в атрибутите style
и on***
се екранира с помощта на
HTML ентичности.
И разбира се, вътре във вложения JavaScript или CSS важат правилата за
екраниране на тези езици. Така че низ в атрибут напр. onload
първо
се екранира според правилата на JS и след това според правилата на HTML
атрибута.
Уф… Както виждате, HTML е много сложен документ, където се наслояват контексти, и без да осъзнаваме къде точно извеждаме данните (т.е. в какъв контекст), не може да се каже как да го направим правилно.
Искате ли пример?
Нека имаме низ Rock'n'Roll
.
Ако го извеждате в HTML текст, точно в този случай не е необходимо да правите никакви замени, защото низът не съдържа нито един знак със специално значение. Друга ситуация възниква, ако го изведете вътре в HTML атрибут, ограден с единични кавички. В такъв случай е необходимо да екранирате кавичките в HTML ентичности:
<div title='Rock'n'Roll'></div>
Това беше просто. Много по-интересна ситуация възниква при наслояване на контексти, например ако низът е част от JavaScript.
Първо ще го изведем в самия JavaScript. Т.е. ще го обвием в кавички и
същевременно ще екранираме с помощта на знака \
кавичките,
съдържащи се в него:
'Rock\'n\'Roll'
Можем още да допълним извикването на някаква функция, за да прави кодът нещо:
alert('Rock\'n\'Roll');
Ако този код вмъкнем в HTML документ с помощта на <script>
, не е
необходимо да се променя нищо друго, защото в него не се среща
забранената последователност </script
:
<script> alert('Rock\'n\'Roll'); </script>
Ако обаче искахме да го вмъкнем в HTML атрибут, трябва още да екранираме кавичките в HTML ентичности:
<div onclick='alert('Rock\'n\'Roll')'></div>
Вложеният контекст обаче не е задължително да бъде само JS или CSS.
Често това е и URL. Параметрите в URL се екранират така, че знаците със
специално значение се преобразуват в последователности, започващи с
%
. Пример:
https://example.org/?a=Jazz&b=Rock%27n%27Roll
И когато този низ изведем в атрибут, ще приложим още екраниране
според този контекст и ще заменим &
с &
:
<a href="https://example.org/?a=Jazz&b=Rock%27n%27Roll">
Ако сте прочели дотук, поздравления, беше изчерпателно. Сега вече имате добра представа какво са контексти и екраниране. И не трябва да се притеснявате, че е сложно. Latte прави това за вас автоматично.
Latte срещу наивни системи
Показахме си как правилно се екранира в HTML документ и колко е важно познаването на контекста, т.е. мястото, където извеждаме данните. С други думи, как работи контекстно-чувствителното екраниране. Въпреки че това е необходима предпоставка за функционална защита срещу XSS, Latte е единствената система за шаблони за PHP, която може това.
Как е възможно това, когато всички системи днес твърдят, че имат автоматично екраниране? Автоматичното екраниране без познаване на контекста е малко глупост, която създава фалшиво усещане за сигурност.
Системи за шаблони като Twig, Laravel Blade и други не виждат в шаблона никаква HTML структура. Следователно не виждат и контексти. В сравнение с Latte те са слепи и наивни. Обработват само собствените си тагове, всичко останало за тях е незначителен поток от знаци:
░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░
░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░
- в текст: <span>{{ foo }}</span>
- в таг: <span {{ foo }} ></span>
- в атрибут: <span title='{{ foo }}'></span>
- в атрибут без кавички: <span title={{ foo }}></span>
- в атрибут, съдържащ URL: <a href="{{ foo }}"></a>
- в атрибут, съдържащ JavaScript: <img onload="{{ foo }}">
- в атрибут, съдържащ CSS: <span style="{{ foo }}"></span>
- в JavaScript: <script>var = {{ foo }}</script>
- в CSS: <style>body { content: {{ foo }}; }</style>
- в коментар: <!-- {{ foo }} -->
Наивните системи само механично преобразуват знаците
< > & ' "
в HTML ентичности, което, макар и в повечето случаи на
употреба да е валиден начин за екраниране, далеч не винаги е така. Те не
могат да открият или предотвратят възникването на различни дупки в
сигурността, както ще покажем по-нататък.
Latte вижда шаблона по същия начин като вас. Разбира HTML, XML, разпознава тагове, атрибути и т.н. И благодарение на това разграничава отделните контексти и според тях обработва данните. Предлага така наистина ефективна защита срещу критичната уязвимост Cross-site Scripting.
░░░░░░░░░░░<span>{$foo}</span>
░░░░░░░░░░<span {$foo} ></span>
░░░░░░░░░░░░░░<span title='{$foo}'></span>
░░░░░░░░░░░░░░░░░░░░░░░░░░░<span title={$foo}></span>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<a href="{$foo}"></a>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<img onload="{$foo}">
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<span style="{$foo}"></span>
░░░░░░░░░░░░░░░░░<script>░░░░░░{$foo}</script>
░░░░░░░░░<style>░░░░░░░░░░░░░░░░{$foo}░░░</style>
░░░░░░░░░░░░░░░<!--░{$foo}░-->
- в текст: <span>{$foo}</span>
- в таг: <span {$foo} ></span>
- в атрибут: <span title='{$foo}'></span>
- в атрибут без кавички: <span title={$foo}></span>
- в атрибут, съдържащ URL: <a href="{$foo}"></a>
- в атрибут, съдържащ JavaScript: <img onload="{$foo}">
- в атрибут, съдържащ CSS: <span style="{$foo}"></span>
- в JavaScript: <script>var = {$foo}</script>
- в CSS: <style>body { content: {$foo}; }</style>
- в коментар: <!-- {$foo} -->
Жив пример
Вляво виждате шаблон в Latte, вдясно е генерираният HTML код. Няколко пъти
тук се извежда променливата $text
и всеки път в малко по-различен
контекст. И следователно и малко по-различно екранирана. Можете сами да
редактирате кода на шаблона, например да промените съдържанието на
променливата и т.н. Опитайте:
Не е ли страхотно! Latte прави контекстно-чувствително екраниране автоматично, така че програмистът:
- не трябва да мисли или да знае как се екранира къде
- не може да сгреши
- не може да забрави за екранирането
Това дори не са всички контексти, които Latte разграничава при извеждане и за които адаптира обработката на данни. Ще разгледаме сега други интересни случаи.
Как да хакнем наивни системи
На няколко практически примера ще покажем колко е важно разграничаването на контексти и защо наивните системи за шаблони не предоставят достатъчна защита срещу XSS, за разлика от Latte. Като представител на наивна система ще използваме в примерите Twig, но същото важи и за други системи.
Уязвимост чрез атрибут
Ще се опитаме да инжектираме в страницата зловреден код с помощта на HTML атрибут, както показахме по-горе. Нека имаме шаблон в Twig, изобразяващ изображение:
<img src={{ imageFile }} alt={{ imageAlt }}>
Забележете, че около стойностите на атрибутите няма кавички. Кодерът може да ги е забравил, което просто се случва. Например в React кодът се пише така, без кавички, и кодер, който сменя езици, след това лесно може да забрави кавичките.
Нападателят като описание на изображението вмъква умело съставен
низ foo onload=alert('Hacked!')
. Вече знаем, че Twig не може да разпознае дали
променливата се извежда в потока на HTML текста, вътре в атрибут, HTML
коментар и т.н., накратко не разграничава контексти. И само механично
преобразува знаците < > & ' "
в HTML ентичности. Така
резултатният код ще изглежда така:
<img src=photo0145.webp alt=foo onload=alert('Hacked!')>
И възникна дупка в сигурността!
Част от страницата стана подправеният атрибут onload
и браузърът
веднага след изтеглянето на изображението го стартира.
Сега ще видим как със същия шаблон ще се справи Latte:
<img src={$imageFile} alt={$imageAlt}>
Latte вижда шаблона по същия начин като вас. За разлика от Twig, разбира HTML и знае, че променливата се извежда като стойност на атрибут, който не е в кавички. Затова ги допълва. Когато нападателят вмъкне същото описание, резултатният код ще изглежда така:
<img src="photo0145.webp" alt="foo onload=alert('Hacked!')">
Latte успешно предотврати XSS.
Извеждане на променлива в JavaScript
Благодарение на контекстно-чувствителното екраниране е напълно нативно възможно да се използват PHP променливи вътре в JavaScript.
<p onclick="alert({$movie})">{$movie}</p>
<script>var movie = {$movie};</script>
Ако променливата $movie
съдържа низ 'Amarcord & 8 1/2'
, ще се
генерира следният изход. Забележете, че вътре в HTML се използва различно
екраниране от това вътре в JavaScript и още по-различно в атрибута
onclick
:
<p onclick="alert("Amarcord & 8 1\/2")">Amarcord & 8 1/2</p>
<script>var movie = "Amarcord & 8 1\/2";</script>
Проверка на връзки
Latte автоматично проверява дали променливата, използвана в атрибутите
src
или href
, съдържа уеб URL (т.е. протокол HTTP) и предотвратява
извеждането на връзки, които могат да представляват риск за
сигурността.
{var $link = 'javascript:attack()'}
<a href={$link}>кликни</a>
Извежда:
<a href="">кликни</a>
Проверката може да се изключи с помощта на филтъра nocheck.
Ограничения на Latte
Latte не е напълно цялостна защита срещу XSS за цялото приложение. Не бихме искали, ако използвате Latte, да спрете да мислите за сигурността. Целта на Latte е да гарантира, че нападателят не може да промени структурата на страницата, да подправи HTML елементи или атрибути. Но не контролира коректността на съдържанието на извежданите данни. Или коректността на поведението на JavaScript. Това вече излиза извън компетенциите на системата за шаблони. Проверката на коректността на данните, особено тези, въведени от потребителя и следователно ненадеждни, е важна задача на програмиста.