Quiz: Czy potrafisz obronić się przed podatnością XSS?

2 lata temu Ze strony David Grudl  

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 &lt;, 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&quot; onclick=&quot;evilCode()">

Rozwiązania dla każdego z przykładów:

  1. znaki < i & reprezentują początek znacznika i encji HTML; zamień je na &lt; oraz. &amp;
  2. znaki " i & reprezentują koniec wartości atrybutu i początek encji HTML; zamień je na &quot; i &amp;
  3. znaki ' i & reprezentują koniec wartości atrybutu i początek encji HTML; zastąp je znakami &apos; i &amp;

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>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script> let foo = &#039;&quot;u{2028}; </script>
❌ <p onclick="foo(&#039;&quot;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>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script>	let foo = &#039;&quot; ; </script>
❌ <p onclick="foo(&#039;&quot; )"></p>


❌ <!-- -- --- -->


❌ <a href="javascript:evilCode()">...</a>


❌❌ <div id="app"> &lt;?php echo e(foo); ?&gt; </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>&lt;'"&amp;</p>
✅ <input value="&lt;&apos;&quot;&amp;">
✅ <input value='&lt;&apos;&quot;&amp;'>


✅ <input value="foo onclick=evilCode()">


✅ <script> let foo = "'\"\n\u2028"; </script>
✅ <p onclick="foo(&quot;&apos;\&quot;\n\u2028&quot;)"></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&apos; 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>