Latte – синоним на сигурност
Latte е единствената система за шаблони на PHP с ефективна защита срещу критичната уязвимост Cross-site Scripting (XSS). Това става благодарение на т.нар. контекстно чувствително ескапиране. Да поговорим,
- как работи уязвимостта XSS и защо е толкова опасна
- какво прави Latte толкова ефективен в защитата срещу XSS
- защо Twig, Blade и други шаблони могат лесно да бъдат компрометирани
Пресичане на сайтове (Cross-Site Scripting – XSS)
Cross-Site Scripting (съкратено XSS) е една от най-често срещаните уязвимости на уебсайтовете, при това много опасна. Той позволява на нападателя да вмъкне зловреден скрипт (известен още като зловреден софтуер) в чужд сайт, който се стартира в браузъра на нищо неподозиращ потребител.
Какво може да прави такъв скрипт? Например може да изпрати на нападателя произволно съдържание от компрометиран сайт, включително чувствителни данни, които се показват след влизане в системата. Той може да променя страницата или да прави други заявки от името на потребителя. Например, ако става въпрос за уебмейл, той може да прочете чувствителни съобщения, да промени показваното съдържание или да промени настройките, като например да позволи препращането на копия на всички съобщения на адреса на нападателя, за да получи достъп до бъдещи имейли.
Ето защо XSS е на първо място в списъка на най-опасните уязвимости. Ако в даден уебсайт бъде открита уязвимост, тя трябва да бъде отстранена възможно най-скоро, за да се предотврати използването ѝ.
Как възниква уязвимостта?
Грешката се появява, когато се генерира уеб страницата и се отпечатват променливи. Представете си, че генерирате страница за търсене и в началото има параграф с термин за търсене във формата:
echo '<p>Search results for <em>' . $search . '</em></p>';
Атакуващият може да напише произволен низ, включително HTML код като
<script>alert("Hacked!")</script>
, в полето за търсене и по този начин в
променливата $search
. Тъй като изходните данни не са обработени по
никакъв начин, те стават част от показаната страница:
<p>Search results for <em><script>alert("Hacked!")</script></em></p>
Вместо да показва низ за търсене, браузърът изпълнява JavaScript. По този начин атакуващият превзема страницата.
Някой може да възрази, че поставянето на код в променлива наистина ще изпълни JavaScript, но само в браузъра на нападателя. И така, как се стига до жертвата? От тази гледна точка можем да разграничим няколко вида XSS. В примера с нашата страница за търсене става дума за отразяващ XSS. В този случай жертвата трябва да бъде подмамена да кликне върху връзка, съдържаща зловреден код в параметъра:
https://example.com/?search=<script>alert("Hacked!")</script>
Въпреки че е необходимо известно социално инженерство, за да
накарате потребителя да кликне върху връзката, това не е трудно.
Потребителите кликват върху линкове, независимо дали в имейли или в
социални медии, без да се замислят. А фактът, че в адреса има нещо
подозрително, може да бъде прикрит от съкратител на URL адреси, така че
потребителят да вижда само bit.ly/xxx
.
Съществува обаче и втора, много по-опасна форма на атака, известна като съхранен XSS или постоянно действащ XSS, при която нападателят успява да запази зловреден код на сървъра, така че той да се вмъква автоматично в определени страници.
Пример за това са сайтове, в които потребителите оставят коментари. Атакуващият изпраща съобщение, съдържащо кода, и то се съхранява на сървъра. Ако сайтът не е достатъчно сигурен, той ще се стартира в браузъра на всеки посетител.
Изглежда, че целта на атаката е да се <script>
линия към
страницата. Всъщност "има много начини за вграждане на JavaScript:https://cheatsheetseries.owasp.org/…t_Sheet.html.
Нека разгледаме пример за вграждане с помощта на HTML атрибут. Да
предположим, че имаме фотогалерия, в която можем да вмъкнем надпис към
изображенията, който се показва в атрибута alt
:
echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';
Атакуващият просто трябва да вмъкне хитро конструирания низ
" onload="alert('Hacked!')
като етикет и ако изходът не е обработен,
полученият код ще изглежда така
<img src="photo0145.webp" alt="" onload="alert('Hacked!')">
Сега подправеният атрибут onload
става част от страницата.
Браузърът ще изпълни съдържащия се в него код веднага след зареждането
на изображението. Хакнат!
Как да се предпазите от XSS?
Всеки опит за откриване на атака с помощта на черен списък, като
например блокиране на <script>
низ и т.н. са недостатъчни.
Основата на ефективната защита е последователното отстраняване на
всички данни, които се визуализират в рамките на страницата.
На първо място, това е заместване на всички символи със специално
значение с други съответстващи последователности, което на жаргон се
нарича escaping (първият символ в последователността се нарича escape
символ, откъдето идва и името). Например в текста на HTML се използва
символът <
has a special meaning, which, if it is not to be interpreted as the beginning of a tag, must be
replaced by a visually corresponding sequence, the so-called HTML entity <
. И браузърът
отпечатва символа.
Изключително важно е да се разграничи контекстът, в който се отпечатват данните. Тъй като в различните контексти низовете се почистват по различен начин. Различните знаци имат специално значение в различни контексти. Например ескапирането на текст в HTML, на атрибути в HTML, на някои специални елементи и т.н. е различно. Скоро ще обсъдим това подробно.
Най-добре е да извършвате бягството директно при писане на ред на страницата, за да сте сигурни, че то наистина се случва и че се случва само веднъж. Най-добре е обработката да се извършва автоматично директно от системата за шаблони. Защото ако обработката не се извършва автоматично, програмистът може да забрави за нея. А един пропуск означава, че сайтът е уязвим.
XSS обаче засяга не само изхода на шаблоните, но и други части на
приложението, които трябва правилно да обработват ненадеждни данни.
Например JavaScript в приложението ви не трябва да използва innerHTML
в
комбинация с тях, а само innerText
или textContent
. Особено
внимание трябва да се обърне на функциите, които оценяват низове, като
например JavaScript, който е eval()
, но и setTimeout()
, или използването
на setAttribute()
с атрибути на събития, като onload
, и т.н. Но това е
извън обхвата на шаблоните.
идеална защита в 3 точки:
- разпознава контекста, в който се извеждат данните
- обработва данните в съответствие с правилата на този контекст (т.е. „ориентиран към контекста“).
- прави това автоматично
Проверка с отчитане на контекста
Какво точно означава думата „контекст“? Това е място в документа със собствени правила за обработка на изхода. Тя зависи от вида на документа (HTML, XML, CSS, JavaScript, обикновен текст, …) и може да варира в определени части на документа. Например в един HTML документ има много места (контексти), където се прилагат напълно различни правила. Може би ще се изненадате от броя им. Ето първите четири:
<p>#text</p>
<img src="#attribute">
<textarea>#rawtext</textarea>
<!-- #comment -->
Първоначалният и основен контекст на една HTML страница е HTML текстът.
Какви са правилата тук? Специалните знаци <
and &
представляват началото на таг или същност, така че те трябва да бъдат
премахнати и заменени със същност на HTML (<
with <
,
&
with &
).
Вторият най-често срещан контекст е стойността на HTML атрибута. Той се
различава от текста по това, че тук кавичките "
or '
, които
отделят атрибута, имат специално значение. Тя трябва да бъде написана
като цяло, за да не се възприема като край на атрибута. От друга страна,
символът <
може спокойно да се използва в атрибута, тъй като
няма специално значение; той не може да се възприеме като начало на таг
или коментар. Но имайте предвид, че в HTML можете да записвате
стойностите на атрибутите без кавички, като в този случай цял набор от
знаци имат специално значение, така че това е друг отделен контекст.
Може би ще се изненадате, но вътре в героите <textarea>
и
<title>
елементи, където <
character need not (but can) be escaped unless
followed by /
. Но това е по-скоро любопитство.
По-интересно е в HTML коментарите. Тук HTML същностите не се използват за ескапиране. Дори няма спецификация, която да определя как да се изписват коментари. Просто трябва да спазвате няколко правила на любопитството и да избягвате определени комбинации от символи в тях.
Контекстите могат да бъдат и многопластови, което се случва, когато вграждаме JavaScript или CSS в HTML. Това може да се направи по два различни начина: с елемент или атрибут:
<script>#js-element</script>
<img onclick="#js-attribute">
<style>#css-element</style>
<p style="#css-attribute"></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
Когато извеждаме този низ в атрибута, все още прилагаме escape в
съответствие с този контекст и го заменяме с &
with
&
:
<a href="https://example.org/?a=Jazz&b=Rock%27n%27Roll">
Ако сте прочели дотук, поздравления, беше досадно. Вече сте разбрали добре какво представляват контекстът и бягството. И не е нужно да се притеснявате, че е сложно. Latte го прави автоматично за вас.
Latte срещу наивни системи
Показахме как правилно да изпълнявате ескапирането в HTML документ и колко е важно да познавате контекста, т.е. мястото, където показвате данните. С други думи, как работи контекстно-чувствителното ескапиране. Въпреки че това е предпоставка за функционална защита срещу XSS, **Latte е единствената система за шаблони за PHP, която прави това.
Как е възможно това, след като всички днешни системи твърдят, че имат автоматичен ескейп? Автоматичното бягство, без да се знае контекстът, е пълна глупост, която създава фалшиво чувство за сигурност.
Системите за шаблониране като Twig, Laravel Blade и други не виждат HTML структурата в шаблона. Затова и те не виждат контекста. В сравнение с Latte те са слепи и наивни. Те работят само със собствената си маркировка, а всичко останало за тях е поток от несъществени символи:
░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░
░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░{{ text }}░░░░
- in text: <span>{{ text }}</span>
- in tag: <span {{ text }} ></span>
- in attribute: <span title='{{ text }}'></span>
- in unquoted attribute: <span title={{ text }}></span>
- in attribute containing URL: <a href="{{ text }}"></a>
- in attribute containing JavaScript: <img onload="{{ text }}">
- in attribute containing CSS: <span style="{{ text }}"></span>
- in JavaScriptu: <script>var = {{ text }}</script>
- in CSS: <style>body { content: {{ text }}; }</style>
- in comment: <!-- {{ text }} -->
Наивните системи просто механично преобразуват символите
< > & ' "
в HTML единици, което е правилният начин за ескейпване
в повечето случаи, но далеч не винаги. По този начин те не могат да
откриват или предотвратяват различни дупки в сигурността, както ще
покажем по-долу.
Latte вижда шаблона по същия начин като вас. Той разбира HTML, XML, разпознава тагове, атрибути и др. Поради това той разпознава контекстите и обработва данните по съответния начин. Следователно тя предлага наистина ефективна защита срещу критичната уязвимост Cross-site Scripting.
Демонстрация на живо
Вляво виждате шаблона в 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!')">
Latt успешно предотврати 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
, съдържа уеб адрес (т.е. HTTP протокол) и
предотвратява записването на връзки, които могат да представляват
риск за сигурността.
{var $link = 'javascript:attack()'}
<a href={$link}>click here</a>
Пише:
<a href="">click here</a>
Проверката може да бъде забранена с помощта на филтъра nocheck.
Ограничения на Latte
Latte не е пълна защита от XSS за цялото приложение. Бихме били щастливи, ако спрете да мислите за сигурността, когато използвате Latte. Целта на Latte е да гарантира, че атакуващият не може да променя структурата на страницата, да подправя елементи или атрибути на HTML. Но тя не проверява дали съдържанието на изхода е правилно. Или че JavaScript се държи правилно. Това е извън обхвата на системата за шаблони. Проверката на коректността на данните, особено на въведените от потребителя и следователно недостоверни данни, е важна задача за програмиста.