Quiz: Czy potrafisz obronić się przed podatnością XSS?
Sprawdź swoją wiedzę i umiejętności w zakresie bezpieczeństwa w tym quizie! Czy potrafisz zapobiec przejęciu przez napastnika kontroli nad stroną HTML?
We wszystkich zadaniach zajmiesz się tym samym pytaniem: jak prawidłowo
wyświetlić zmienną $str
na stronie HTML bez tworzenia luki XSS. Podstawą obrony
jest escaping, czyli zastępowanie znaków o specjalnym znaczeniu
odpowiednimi ciągami. Przykładowo, wyprowadzając ciąg znaków do tekstu
HTML, w którym znak <
ma specjalne znaczenie (wskazuje na
początek znacznika), zastępujemy go encją HTML <
, a
przeglądarka poprawnie wyświetla symbol <
.
Bądź czujny, gdyż luka XSS jest bardzo poważna. Może spowodować przejęcie przez napastnika kontroli nad stroną, a nawet kontem użytkownika. Powodzenia i oby udało Ci się zachować bezpieczeństwo strony HTML!
Pierwsza trójka pytań
Określ, które znaki muszą być obsługiwane i jak w pierwszym, drugim i trzecim przykładzie:
1) <p><?= $str ?></p>
2) <input value="<?= $str ?>">
3) <input value='<?= $str ?>'>
Gdyby wyjście nie zostało w żaden sposób potraktowane, stałoby się
częścią wyświetlanej strony. Gdyby atakującemu udało się wstawić ciąg
'foo" onclick="evilCode()'
do zmiennej, a dane wyjściowe nie
zostały potraktowane, spowodowałoby to wykonanie ich kodu po kliknięciu na
element:
$str = 'foo" onclick="evilCode()'
❌ not treated: <input value="foo" onclick="evilCode()">
✅ treated: <input value="foo" onclick="evilCode()">
Rozwiązania dla każdego z przykładów:
- znaki
<
i&
reprezentują początek znacznika i encji HTML; zamień je na<
oraz.&
- znaki
"
i&
reprezentują koniec wartości atrybutu i początek encji HTML; zamień je na"
i&
- znaki
'
i&
reprezentują koniec wartości atrybutu i początek encji HTML; zastąp je znakami'
i&
Za każdą poprawną odpowiedź otrzymujesz punkt. Oczywiście we wszystkich trzech przypadkach możesz zastąpić inne znaki również encjami; nie powoduje to żadnej szkody, ale nie jest konieczne.
Pytanie nr 4
Przechodząc dalej, które znaki należy zastąpić podczas wyświetlania zmiennej w tym kontekście?
<input value=<?= $str ?>>
Rozwiązanie: Jak widać, brakuje tutaj cudzysłowów. Najprostszym sposobem
jest po prostu dodanie cytatów, a następnie ucieczka, jak w poprzednim
pytaniu. Istnieje również drugie rozwiązanie, które polega na zastąpieniu
spacji i wszystkich znaków, które mają specjalne znaczenie wewnątrz
znacznika, takich jak >
, /
, =
, i niektóre
inne z encjami HTML.
Pytanie nr 5
Teraz robi się ciekawiej. Które postacie trzeba potraktować w tym kontekście:
<script>
let foo = '<?= $str ?>';
</script>
Rozwiązanie: Wewnątrz m.in. <script>
znacznika, reguły
ucieczki są określane przez JavaScript. Nie stosuje się tu encji HTML, ale
jest jedna specjalna zasada. Jakie więc znaki uciekamy? Wewnątrz łańcucha
JavaScript, naturalnie uciekamy od znaku '
, który go ogranicza,
używając backslasha, zastępując go znakiem \'
. Ponieważ
JavaScript nie obsługuje łańcuchów wieloliniowych (z wyjątkiem literałów
szablonów), musimy również uciec od znaków nowej linii. Należy jednak
pamiętać, że oprócz zwykłych znaków \n
i \r
,
JavaScript traktuje również znaki Unicode \u2028
i
\u2029
jako znaki nowej linii, z których również musimy uciec.
Na koniec wspomniana zasada specjalna: ciąg nie może zawierać
</script
. Można temu zapobiec, na przykład zastępując go
znakiem <\/script
.
Jeśli wiedziałeś o tym, to gratuluję.
Pytanie nr 6
Poniższy kontekst wydaje się być tylko wariacją poprzedniego. Czy uważasz, że leczenie będzie inne?
<p onclick="foo('<?= $str ?>')"></p>
Rozwiązanie: Ponownie mają tu zastosowanie zasady ucieczki dla ciągów
JavaScript, ale w przeciwieństwie do poprzedniego kontekstu, w którym encje
HTML nie były ucieczką, tutaj są one ucieczką. Tak więc najpierw uciekamy
od łańcucha JavaScript używając backslashes, a następnie zastępujemy znaki
specjalne ("
i &
) encjami HTML. Bądź ostrożny,
właściwa kolejność jest ważna.
Jak widać, ten sam literał JavaScript może być inaczej zakodowany w
elemencie <script>
a inaczej w atrybucie!
Pytanie nr 7
Wróćmy z JavaScript 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) mogą pojawić się
wszystkie tradycyjne znaki specjalne, takie jak <
,
&
, "
i '
. To, co jest zabronione,
i to może Cię zaskoczyć, to para znaków --
. Ucieczka od tej
sekwencji nie jest określona, więc od Ciebie zależy, jak ją zastąpisz.
Możesz je przeplatać spacjami. Albo, na przykład, zastąpić je znakiem
==
.
Pytanie nr 8
Zbliżamy się do końca, więc spróbujmy urozmaicić pytanie. Spróbuj zastanowić się, na co trzeba uważać przy wypisywaniu zmiennej w tym kontekście:
<a href="<?= $str ?>">...</a>
Rozwiązanie: Oprócz ucieczki należy również sprawdzić, czy adres URL
nie zawiera niebezpiecznego schematu, takiego jak javascript:
,
ponieważ tak skomponowany adres URL po kliknięciu wykonywałby kod
atakującego.
Pytanie nr 9.
Na koniec gratka dla prawdziwych koneserów. Oto przykład aplikacji
wykorzystującej nowoczesny framework JavaScript, a konkretnie Vue. Zobaczmy,
czy uda Ci się ustalić, na co należy uważać przy wypisywaniu zmiennej
wewnątrz elementu #app
:
<div id="app">
<?= $str ?>
</div>
<script src="https://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 interpretuje zawartość tego elementu jako swój
szablon. A wewnątrz szablonu interpretuje
podwójne nawiasy klamrowe, które reprezentują zmienne wyjściowe lub
wywołanie kodu JavaScript (np, {{ foo }}
).
Tak więc w obrębie elementu #app
oprócz znaków
<
i &
, specjalne znaczenie ma również para
{{
, którą musimy zastąpić innym odpowiednim ciągiem, aby Vue
nie interpretowało jej jako własnego znacznika. Zastępowanie encjami HTML nie
pomaga w tym przypadku. Jak sobie z tym poradzić? Jest pewna sztuczka:
wstawiamy pusty komentarz HTML pomiędzy "nawiasami klamrowymi
{<!-- -->{
, a Vue zignoruje ten ciąg.
Wyniki quizu
Jak Ci poszło w quizie? Ile masz poprawnych odpowiedzi? Jeśli odpowiedziałeś poprawnie na co najmniej 4 pytania, jesteś w grupie 8% rozwiązujących – gratulujemy!
**Zapewnienie bezpieczeństwa Twojej witryny wymaga jednak odpowiedniego postępowania z danymi wyjściowymi w każdej sytuacji.
Jeśli zaskoczyło Cię to, jak wiele różnych kontekstów może pojawić się na typowej stronie HTML, wiedz, że nie wymieniliśmy jeszcze wszystkich. To sprawiłoby, że quiz byłby znacznie dłuższy. Niemniej jednak, nie musisz być ekspertem w ucieczce w każdym kontekście, jeśli twój system szablonów może sobie z tym poradzić.
Przetestujmy je więc.
Jak sprawują się systemy szablonowania?
Wszystkie nowoczesne systemy templatek posiadają funkcję autoescaping, która automatycznie ucieka od wszystkich zmiennych na wyjściu. Jeśli robią to poprawnie, Twoja strona jest bezpieczna. Jeśli robią to źle, strona jest narażona na ryzyko wystąpienia podatności XSS ze wszystkimi jej poważnymi konsekwencjami.
Przetestujemy popularne systemy szablonów z pytań w tym quizie, aby określić skuteczność ich auto-escapingu. Niech więc rozpocznie się przegląd systemów templatek w PHP.
Twig ❌
Na początek system templatkowania Twig (wersja 3.5), najczęściej używany w
połączeniu z frameworkiem Symfony. Zadamy mu zadanie odpowiedzenia na
wszystkie pytania quizu. Zmienna $str
będzie zawsze wypełniona
podchwytliwym ciągiem znaków, a my zobaczymy jak radzi sobie z wyjściem.
Możesz zobaczyć wyniki po prawej stronie. Możesz również zbadać jego
odpowiedzi i zachowanie 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 nie zdał w sześciu z dziewięciu testów!
Niestety, automatyczne uciekanie Twiga działa tylko w tekście HTML i atrybutach, a nawet wtedy tylko wtedy, gdy są one ujęte w cudzysłów. 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 zapisywane są wartości atrybutów w popularnych bibliotekach takich jak React czy Svelte. Programista, który używa zarówno Twig, jak i React, może dość naturalnie zapomnieć o cudzysłowach.
Autoescaping Twiga zawodzi również we wszystkich innych przykładach. W
kontekstach (5) i (6) potrzebna jest ręczna ucieczka przy użyciu
{{ str|escape('js') }}
, podczas gdy dla innych kontekstów Twig nie
oferuje nawet funkcji ucieczki. Brakuje również ochrony przed drukowaniem
złośliwego linku (8) lub wsparcia dla szablonów Vue (9).
Blade ❌❌
Drugim uczestnikiem jest system templatowania Blade (wersja 10.9), który jest ściśle zintegrowany z Laravel i jego ekosystemem. Ponownie sprawdzimy jego możliwości na naszych pytaniach quizowych. Można też poznać jego odpowiedzi 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 do Twiga. Ponownie, automatyczne escaping działa tylko w
tekście HTML i atrybutach i tylko wtedy, gdy są one zamknięte w
cudzysłowach. Automatyczne escaping Blade'a zawodzi również we wszystkich
innych przykładach. W kontekstach (5) i (6) potrzebna jest ręczna ucieczka
przy użyciu {{ Js::from($str) }}
. W pozostałych kontekstach Blade
nie oferuje nawet funkcji ucieczki. Brakuje również ochrony przed drukowaniem
złośliwych linków (8) oraz wsparcia dla szablonów Vue (9).
Zaskakująca jest jednak porażka dyrektywy @php
w Blade, która
powoduje wyprowadzenie własnego kodu PHP bezpośrednio na wyjście, co widać w
ostatniej linii.
Latte ✅
Trio jest zakończone systemem templatkowania Latte (wersja 3.0). Przetestujemy jego autoescaping. Można też poznać jego odpowiedzi i zachowanie 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 doskonale poradził sobie ze wszystkimi dziewięcioma zadaniami!
Poradził sobie z brakującymi cudzysłowami w atrybutach HTML, przetworzył
JavaScript zarówno w <script>
elemencie, jak i w
atrybutach, oraz poradził sobie z niedozwolonym ciągiem w
komentarzach HTML.
Co więcej, zapobiegło sytuacji, w której kliknięcie na złośliwy link dostarczony przez atakującego mogłoby spowodować wykonanie jego kodu. I poradziło sobie z escapingiem znaczników dla Vue.
Test bonusowy
Jedną z istotnych możliwości wszystkich systemów templatek jest praca
z blokami i związane z tym dziedziczenie szablonów. Dlatego wszystkim
testowanym systemom templatkowania postawimy jeszcze jedno zadanie. Stworzymy
blok description
, który wypiszemy w atrybucie HTML. W prawdziwym
świecie definicja bloku znajdowałaby się oczywiście w szablonie dziecka, a
jego wyjście w szablonie rodzica, np. w layoucie. To tylko uproszczona forma,
ale wystarczy, aby przetestować autoescaping przy wyprowadzaniu bloków. Jak
wypadły?
Twig: failed ❌ przy wyprowadzaniu bloków, znaki nie są poprawnie escaped
{% block description %}
rock n' roll
{% endblock %}
<meta name='description'
content='{{ block('description') }}'>
<meta name='description'
content=' rock n' roll '> ❌
Blade: failed ❌ podczas wyprowadzania bloków, znaki nie są prawidłowo escape'owane
@section('description')
rock n' roll
@endsection
<meta name='description'
content='@yield('description')'>
<meta name='description'
content=' rock n' roll '> ❌
Latte: przeszedł ✅ przy wyprowadzaniu bloków, poprawnie obsługiwał 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 internetowych jest podatnych na ataki?
Autoescaping w systemach takich jak Twig, Blade czy Smarty działa poprzez
proste zastąpienie pięciu znaków <>"'&
encjami HTML
i nie rozróżnia kontekstu. Dlatego działa tylko w niektórych sytuacjach
i zawodzi we wszystkich innych. Nawykowy autoescaping jest niebezpieczną
funkcją, ponieważ tworzy fałszywe poczucie bezpieczeństwa.
Nie dziwi więc fakt, że obecnie ponad 27% stron internetowych posiada krytyczne podatności, głównie XSS (źródło: Acunetix Web Vulnerability Report). Jak wyjść z tej sytuacji? Zastosuj system templatkowania, który rozróżnia konteksty.
Latte jest jedynym systemem templatowania w PHP, który nie postrzega szablonu jako tylko ciągu znaków, ale rozumie HTML. Wie, czym są tagi, atrybuty itp. Rozróżnia konteksty. I dlatego poprawnie ucieka w tekst HTML, inaczej wewnątrz znaczników HTML, inaczej wewnątrz JavaScript, itd.
Latte stanowi więc jedyny bezpieczny system templatkowania.
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ć