Quiz: czy obronisz się przed podatnością XSS?
Sprawdź swoją wiedzę w quizie bezpieczeństwa! Czy potrafisz zapobiec przejęciu kontroli nad stroną HTML przez atakującego?

We wszystkich zadaniach będziesz rozwiązywać to samo pytanie: jak
poprawnie wypisać zmienną $str
na stronie HTML, aby nie powstała
podatność XSS. Podstawą
obrony jest escapowanie, co oznacza zastępowanie znaków o specjalnym
znaczeniu odpowiednimi sekwencjami. Na przykład przy wypisywaniu ciągu znaków
do tekstu HTML, w którym znak <
ma specjalne znaczenie
(sygnalizuje początek znacznika), zastępujemy go encją HTML
<
, a przeglądarka poprawnie wyświetli symbol
<
.
Bądź ostrożny, ponieważ podatność XSS jest bardzo poważna. Może spowodować, że atakujący przejmie kontrolę nad stroną lub nawet kontem użytkownika. Powodzenia i oby udało Ci się utrzymać stronę HTML w bezpieczeństwie!
Pierwsze trzy pytania
Podaj, jakie znaki i w jaki sposób należy zabezpieczyć w pierwszym, drugim i trzecim przykładzie:
1) <p><?= $str ?></p>
2) <input value="<?= $str ?>">
3) <input value='<?= $str ?>'>
Gdyby dane wyjściowe nie zostały w żaden sposób zabezpieczone, stałyby
się częścią wyświetlanej strony. Gdyby atakujący umieścił w zmiennej
ciąg znaków 'foo" onclick="evilCode()'
i dane wyjściowe nie
zostałyby zabezpieczone, spowodowałoby to, że po kliknięciu na element
uruchomi się jego kod:
$str = 'foo" onclick="evilCode()'
❌ bez zabezpieczenia: <input value="foo" onclick="evilCode()">
✅ z zabezpieczeniem: <input value="foo" onclick="evilCode()">
Rozwiązanie poszczególnych przykładów:
- znaki
<
i&
reprezentują początek tagu HTML i encji, zastępujemy je przez<
i&
- znaki
"
i&
reprezentują koniec wartości atrybutu i początek encji HTML, zastępujemy je przez"
i&
- znaki
'
i&
reprezentują koniec wartości atrybutu i początek encji HTML, zastępujemy je przez'
i&
Za każdą poprawną odpowiedź otrzymujesz punkt. Oczywiście we wszystkich trzech przypadkach można zastępować encjami również inne znaki, niczemu to nie przeszkadza, ale nie jest to konieczne.
Pytanie nr 4
Kontynuujemy dalej. Jakie znaki należy zastąpić podczas wypisywania zmiennej w tym kontekście?
<input value=<?= $str ?>>
Rozwiązanie: Jak widzisz, tutaj brakuje cudzysłowów. Najłatwiej jest po
prostu dodać cudzysłowy, a następnie escapować tak samo jak w poprzednim
pytaniu. Istnieje również drugie rozwiązanie, a mianowicie zastąpienie w
ciągu znaków spacją i wszystkimi znakami, które mają specjalne znaczenie
wewnątrz tagu, tj. >
, /
, =
i niektóre
inne, encjami HTML.
Pytanie nr 5
Teraz zaczyna być ciekawiej. Jakie znaki należy zabezpieczyć w tym kontekście:
<script>
let foo = '<?= $str ?>';
</script>
Rozwiązanie: Wewnątrz znacznika <script>
zasady
escapowania określa JavaScript. Encje HTML nie są tutaj używane,
jednak obowiązuje jedna specjalna zasada. Jakie więc znaki escapujemy?
Wewnątrz ciągu znaków JavaScript escapujemy oczywiście znak '
,
który go ogranicza, za pomocą ukośnika, czyli zastępujemy go przez
\'
. Ponieważ JavaScript nie obsługuje ciągów wieloliniowych
(tylko jako template
literal), musimy escapować również znaki końca linii. Jednak uwaga,
oprócz zwykłych znaków \n
i \r
JavaScript uważa za
końce linii również znaki unicode \u2028
i \u2029
,
które również musimy escapować. I na koniec wspomniana specjalna zasada: w
ciągu znaków nie może wystąpić </script
. Można temu
zapobiec np. zastępując przez <\/script
.
Jeśli to wiedziałeś, gratulujemy.
Pytanie nr 6
Następujący kontekst wygląda tylko jak wariacja poprzedniego. Co myślisz, czy zabezpieczanie będzie się różnić?
<p onclick="foo('<?= $str ?>')"></p>
Rozwiązanie: Ponownie obowiązują tutaj zasady escapowania w ciągach
JavaScript, ale w przeciwieństwie do poprzedniego kontekstu, gdzie nie
escapowano za pomocą encji HTML, tutaj wręcz przeciwnie, escapuje się. Czyli
najpierw escapujemy ciąg JavaScript za pomocą ukośników, a następnie
zastępujemy znaki o specjalnym znaczeniu ("
i &
)
encjami HTML. Uwaga, poprawna kolejność jest ważna.
Jak widzisz, ten sam literał JavaScript może być zakodowany inaczej w
elemencie <script>
, a inaczej w atrybucie!
Pytanie nr 7
Wróćmy z JavaScriptu z powrotem do HTML. Jakie znaki musimy zastąpić wewnątrz komentarza i w jaki sposób?
<!-- <?= $str ?> -->
Rozwiązanie: wewnątrz komentarza HTML (i XML) wszystkie tradycyjne znaki
specjalne, takie jak <
, &
, "
i
'
, mogą się pojawiać. Zakazana jest, i to cię pewnie zaskoczy,
para znaków --
. Escapowanie tej sekwencji nie jest specyfikowane,
więc to od ciebie zależy, w jaki sposób ją zastąpisz. Możesz je
rozdzielić spacjami. Albo na przykład zastąpić przez ==
.
Pytanie nr 8
Już zbliżamy się do końca, więc spróbujemy zmodyfikować pytanie. Spróbuj zastanowić się, na co należy zwrócić uwagę podczas wypisywania zmiennej w tym kontekście:
<a href="<?= $str ?>">...</a>
Rozwiązanie: oprócz escapowania ważne jest jeszcze sprawdzenie, czy URL
nie zawiera niebezpiecznego schematu jak javascript:
, ponieważ tak
skonstruowany URL po kliknięciu wywołałby kod atakującego.
Pytanie nr 9
Na zakończenie perełka dla prawdziwych koneserów. Jest to przykład
aplikacji wykorzystującej nowoczesny framework JavaScript, konkretnie Vue.
Ciekawe, czy wpadniesz na to, na co zwrócić uwagę przy wypisywaniu zmiennej
wewnątrz elementu #app
:
<div id="app">
<?= $str ?>
</div>
<script src="http://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
...
})
</script>
Ten kod tworzy aplikację Vue, która będzie renderowana w elemencie
#app
. Vue zawartość tego elementu traktuje jako swój szablon.
A wewnątrz szablonu interpretuje
podwójne nawiasy klamrowe, które oznaczają wypisanie zmiennej lub wywołanie
kodu JavaScript (np. {{ foo }}
).
Zatem wewnątrz elementu #app
oprócz znaków <
i &
jeszcze specjalne znaczenie ma para {{
,
którą musimy zastąpić inną odpowiednią sekwencją, aby Vue nie
interpretowało jej jako swojego znacznika. Zastąpienie encjami HTML jednak w
tym przypadku nie pomoże. Jak sobie poradzić? Działa sztuczka: między
nawiasy wstawiamy pusty komentarz HTML {<!-- -->{
i Vue
taką sekwencję ignoruje.
Wyniki quizu
Jak Ci poszło w quizie? Ile masz poprawnych odpowiedzi? Jeśli odpowiedziałeś poprawnie na co najmniej 4 pytania, należysz do 8% najlepszych rozwiązujących – gratulujemy!
Jednak aby zapewnić bezpieczeństwo Twojej strony internetowej, niezbędne jest poprawne zabezpieczenie danych wyjściowych we wszystkich sytuacjach.
Jeśli zaskoczyło Cię, ile różnych kontekstów może wystąpić na zwykłej stronie HTML, to wiedz, że nie wymieniliśmy wszystkich. To byłby znacznie dłuższy quiz. Mimo to nie musisz być ekspertem od escapowania w każdym kontekście, jeśli potrafi to Twój system szablonów.
Sprawdźmy je więc.
Jak radzą sobie systemy szablonów?
Wszystkie nowoczesne systemy szablonów chwalą się funkcją autoescapowania, która automatycznie escapuje wszystkie wypisywane zmienne. Jeśli robią to poprawnie, Twoja strona jest bezpieczna. Jeśli robią to źle, strona jest narażona na ryzyko podatności XSS ze wszystkimi poważnymi konsekwencjami.
Przetestujemy popularne systemy szablonów z pytań tego quizu, aby sprawdzić, jak skuteczne jest ich autoescapowanie. Zaczyna się dTest systemów szablonów dla PHP.
Twig ❌
Pierwszy na liście jest system szablonów Twig (wersja 3.5), który najczęściej
używany jest w połączeniu z frameworkiem Symfony. Zlecimy mu zadanie
odpowiedzi na wszystkie pytania quizu. Zmienna $str
będzie zawsze
wypełniona podstępnym ciągiem znaków i zobaczymy, jak poradzi sobie z jego
wypisaniem. Wyniki widzisz po prawej stronie. Możesz jego odpowiedzi
i zachowanie również zbadać na placu zabaw.
{% set str = "<'\"&" %}
1) <p>{{ str }}</p>
2) <input value="{{ str }}">
3) <input value='{{ str }}'>
{% set str = "foo onclick=evilCode()" %}
4) <input value={{ str }}>
{% set str = "'\"\n\u{2028}" %}
5) <script> let foo = '{{ str }}'; </script>
6) <p onclick="foo('{{ str }}')"></p>
{% set str = "-- ---" %}
7) <!-- {{ str }} -->
{% set str = "javascript:evilCode()" %}
8) <a href="{{ str }}">...</a>
{% set str = "{{ foo }}" %}
9) <div id="app"> {{ str }} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '"u{2028}; </script>
❌ <p onclick="foo('"u{2028})"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌ <div id="app"> {{ foo }} </div>
Twig zawiódł w sześciu z dziewięciu testów!
Niestety, automatyczne escapowanie Twiga działa tylko w tekście HTML i atrybutach, a do tego tylko wtedy, gdy są one zamknięte w cudzysłowy. Gdy tylko brakuje cudzysłowów, Twig nie zgłasza żadnego błędu i tworzy dziurę bezpieczeństwa XSS.
Jest to szczególnie nieprzyjemne, ponieważ w ten sposób wartości atrybutów zapisuje się w popularnych bibliotekach, takich jak React czy Svelte. Programista, który jednocześnie używa Twiga i Reacta, może więc całkiem naturalnie zapomnieć o cudzysłowach.
Autoescapowanie Twiga zawodzi również we wszystkich pozostałych
przykładach. W kontekstach (5) i (6) trzeba escapować ręcznie za pomocą
{{ str|escape('js') }}
, dla pozostałych kontekstów Twig funkcji
escapującej nawet nie oferuje. Nie dysponuje również ochroną przed
wypisaniem szkodliwego linku (8) ani wsparciem dla szablonów Vue (9).
Blade ❌❌
Drugim uczestnikiem jest system szablonów Blade (wersja 10.9), który jest ściśle zintegrowany z Laravelem i jego ekosystemem. Ponownie sprawdzimy jego możliwości na naszych pytaniach quizu. Jego odpowiedzi możesz również zbadać na placu zabaw.
@php($str = "<'\"&")
1) <p>{{ $str }}</p>
2) <input value="{{ $str }}">
3) <input value='{{ $str }}'>
@php($str = "foo onclick=evilCode()")
4) <input value={{ $str }}>
@php($str = "'\"\n\u{2028}")
5) <script> let foo = {{ $str }}; </script>
6) <p onclick="foo({{ $str }})"></p>
@php($str = "-- ---")
7) <!-- {{ $str }} -->
@php($str = "javascript:evilCode()")
8) <a href="{{ $str }}">...</a>
@php($str = "{{ foo }}")
9) <div id="app"> {{ $str }} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '" ; </script>
❌ <p onclick="foo('" )"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌❌ <div id="app"> <?php echo e(foo); ?> </div>
Blade zawiódł w sześciu z dziewięciu testów!
Wynik jest podobny jak w przypadku Twiga. Ponownie obowiązuje zasada, że
automatyczne escapowanie działa tylko w tekście HTML i atrybutach, i tylko
jeśli są one zamknięte w cudzysłowy. Autoescapowanie Blade zawodzi również
we wszystkich pozostałych przykładach. W kontekstach (5) i (6) konieczne jest
escapowanie ręczne za pomocą {{ Js::from($str) }}
. Dla
pozostałych kontekstów Blade funkcji escapującej nawet nie oferuje. Nie
dysponuje ochroną przed wypisaniem szkodliwego linku (8) ani wsparciem dla
szablonów Vue (9).
Co jest jednak zaskakujące, to awaria dyrektywy @php
w Blade,
co powoduje wypisanie własnego kodu PHP bezpośrednio na wyjście, co widać w
ostatnim wierszu.
Smarty ❌❌❌
Teraz przetestujemy najstarszy system szablonów dla PHP, którym jest Smarty (wersja 4.3). Ku wielkiemu zaskoczeniu
ten system nie ma aktywnego automatycznego escapowania. Musisz więc przy
wypisywaniu zmiennych albo za każdym razem podać filtr
{$var|escape}
, albo aktywować automatyczne escapowanie HTML.
Informacja o tym jest w dokumentacji dość ukryta.
{$str = "<'\"&"}
1) <p>{$str}</p>
2) <input value="{$str}">
3) <input value='{$str}'>
{$str = "foo onclick=evilCode()"}
4) <input value={$str}>
{$str = "'\"\n\u{2028}"}
5) <script> let foo = {$str}; </script>
6) <p onclick="foo({$str})"></p>
{$str = "-- ---"}
7) <!-- {$str} -->
{$str = "javascript:evilCode()"}
8) <a href="{$str}">...</a>
{$str = "{{ foo }}"}
9) <div id="app"> {$str} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '"\u2028; </script>
❌ <p onclick="foo('"\u2028)"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌ <div id="app"> {{ foo }} </div>
Smarty zawiodły w sześciu z dziewięciu testów!
Wynik jest na pierwszy rzut oka podobny jak w przypadku poprzednich
bibliotek. Smarty potrafią automatycznie escapować tylko w tekście HTML
i atrybutach, i to tylko wtedy, gdy wartości są zamknięte w cudzysłowy.
Wszędzie indziej zawodzą. W kontekstach (5) i (6) konieczne jest escapowanie
ręczne za pomocą {$str|escape:javascript}
. Jednak jest to
możliwe tylko wtedy, gdy nie jest aktywne automatyczne escapowanie HTML,
inaczej bowiem te escapowania kolidują ze sobą. Smarty są więc z punktu
widzenia bezpieczeństwa całkowitą porażką tego testu.
Latte ✅
Trójkę zamyka system szablonów Latte (wersja 3.0). Wypróbujemy jego autoescapowanie. Jego odpowiedzi i zachowanie możesz również zbadać na placu zabaw.
{var $str = "<'\"&"}
1) <p>{$str}</p>
2) <input value="{$str}">
3) <input value='{$str}'>
{var $str = "foo onclick=evilCode()"}
4) <input value={$str}>
{var $str = "'\"\n\u{2028}"}
5) <script> let foo = {$str}; </script>
6) <p onclick="foo({$str})"></p>
{var $str = "-- ---"}
7) <!-- {$str} -->
{var $str = "javascript:evilCode()"}
8) <a href="{$str}">...</a>
{var $str = "{{ foo }}"}
9) <div id="app"> {$str} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
✅ <input value="foo onclick=evilCode()">
✅ <script> let foo = "'\"\n\u2028"; </script>
✅ <p onclick="foo("'\"\n\u2028")"></p>
✅ <!-- - - - - - -->
✅ <a href="">...</a>
✅ <div id="app"> {<!-- -->{ foo }} </div>
Latte we wszystkich dziewięciu zadaniach wypadło znakomicie!
Poradziło sobie z brakującymi cudzysłowami przy atrybutach HTML,
poradziło sobie z przetwarzaniem JavaScriptu zarówno w elemencie
<script>
, jak i w atrybutach, i potrafiło poradzić sobie
również z zakazaną sekwencją w komentarzach HTML.
Co więcej, zapobiegło sytuacji, w której kliknięcie na podstawiony link przez atakującego mogłoby uruchomić jego kod. I poradziło sobie z escapowaniem znaczników dla Vue.
Test bonusowy
Jedną z istotnych zdolności wszystkich systemów szablonów jest praca
z blokami i związana z tym dziedziczenie szablonów. Spróbujemy więc dać
wszystkim testowanym systemom szablonów jeszcze jedno zadanie. Stworzymy blok
description
, który wypiszemy w atrybucie HTML. W realnym świecie
oczywiście definicja bloku znajdowałaby się w szablonie potomnym, a jego
wypisanie w szablonie nadrzędnym, czyli na przykład layoucie. To jest tylko
uproszczona forma, ale wystarczy do przetestowania autoescapowania przy
wypisywaniu bloków. Jak sobie poradzili?
Twig: zawiódł ❌ przy wypisywaniu bloków znaków nie zabezpiecza
{% block description %}
rock n' roll
{% endblock %}
<meta name='description'
content='{{ block('description') }}'>
<meta name='description'
content=' rock n' roll '> ❌
Blade: zawiódł ❌ przy wypisywaniu bloków znaków nie zabezpiecza
@section('description')
rock n' roll
@endsection
<meta name='description'
content='@yield('description')'>
<meta name='description'
content=' rock n' roll '> ❌
Latte: zdał ✅ przy wypisywaniu bloków poprawnie zabezpieczył problematyczne znaki
{block description}
rock n' roll
{/block}
<meta name='description'
content='{include description}'>
<meta name='description'
content=' rock n' roll '> ✅
Dlaczego tak wiele stron jest podatnych na ataki?
Autoescapowanie w systemach takich jak Twig, Blade czy Smarty działa tak,
że po prostu zastępuje pięć znaków <>"'&
encjami
HTML i w żaden sposób nie rozróżnia kontekstu. Dlatego działa tylko w
niektórych sytuacjach, a we wszystkich pozostałych zawodzi. Naiwne
autoescapowanie jest niebezpieczną funkcją, ponieważ tworzy fałszywe
poczucie bezpieczeństwa.
Nie jest więc zaskakujące, że obecnie ponad 27% stron internetowych ma krytyczne podatności, przede wszystkim XSS (źródło: Acunetix Web Vulnerability Report). Jak z tego wyjść? Użyć systemu szablonów, który rozróżnia konteksty.
Latte jest jedynym systemem szablonów w PHP, który nie postrzega szablonu tylko jako ciągu znaków, ale rozumie HTML. Rozumie, co to są znaczniki, atrybuty itd. Rozróżnia konteksty. I dlatego poprawnie escapuje w tekście HTML, inaczej wewnątrz znacznika HTML, inaczej wewnątrz JavaScriptu itd.
Latte stanowi więc jedyny bezpieczny system szablonów.
Ponadto, dzięki zrozumieniu HTML, oferuje wspaniałe n:atrybuty, które użytkownicy uwielbiają:
<ul n:if="$menu">
<li n:foreach="$menu->getItems() as $item">{$item->title}</li>
</ul>
Aby przesłać komentarz, proszę się zalogować