Quiz: czy obronisz się przed podatnością XSS?

2 lata temu przez David Grudl  

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

Rozwiązanie poszczególnych przykładów:

  1. znaki < i & reprezentują początek tagu HTML i encji, zastępujemy je przez &lt; i &amp;
  2. znaki " i & reprezentują koniec wartości atrybutu i początek encji HTML, zastępujemy je przez &quot; i &amp;
  3. znaki ' i & reprezentują koniec wartości atrybutu i początek encji HTML, zastępujemy je przez &apos; i &amp;

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>&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 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>&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 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>&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;\u2028; </script>
❌ <p onclick="foo(&#039;&quot;\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>&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 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&apos; 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>