Quiz: Können Sie sich vor der XSS-Schwachstelle schützen?

vor 2 Jahren von David Grudl  

Testen Sie Ihr Wissen im Sicherheitsquiz! Können Sie verhindern, dass ein Angreifer die Kontrolle über eine HTML-Seite übernimmt?

In allen Aufgaben lösen Sie dieselbe Frage: Wie gibt man die Variable $str korrekt auf einer HTML-Seite aus, damit keine XSS-Schwachstelle entsteht? Die Grundlage der Verteidigung ist das Escaping, was bedeutet, Zeichen mit besonderer Bedeutung durch entsprechende Sequenzen zu ersetzen. Zum Beispiel wird beim Ausgeben einer Zeichenkette in HTML-Text, in dem das Zeichen < eine besondere Bedeutung hat (signalisiert den Anfang eines Tags), dieses durch die HTML-Entität &lt; ersetzt, und der Browser zeigt das Symbol < korrekt an.

Seien Sie auf der Hut, denn die XSS-Schwachstelle ist sehr ernst. Sie kann dazu führen, dass ein Angreifer die Kontrolle über die Seite oder sogar über das Benutzerkonto übernimmt. Viel Erfolg und möge es Ihnen gelingen, die HTML-Seite sicher zu halten!

Die ersten drei Fragen

Geben Sie an, welche Zeichen und auf welche Weise im ersten, zweiten und dritten Beispiel behandelt werden müssen:

1) <p><?= $str ?></p>
2) <input value="<?= $str ?>">
3) <input value='<?= $str ?>'>

Wenn die Ausgabe nicht behandelt würde, würde sie Teil der angezeigten Seite werden. Wenn ein Angreifer die Zeichenkette 'foo" onclick="evilCode()' in die Variable einschleusen würde und die Ausgabe nicht behandelt würde, würde dies dazu führen, dass beim Klicken auf das Element sein Code ausgeführt wird:

$str = 'foo" onclick="evilCode()'
❌ ohne Behandlung: <input value="foo" onclick="evilCode()">
✅ mit Behandlung:  <input value="foo&quot; onclick=&quot;evilCode()">

Lösung der einzelnen Beispiele:

  1. Die Zeichen < und & stellen den Anfang eines HTML-Tags und einer Entität dar, wir ersetzen sie durch &lt; und &amp;.
  2. Die Zeichen " und & stellen das Ende des Attributwerts und den Anfang einer HTML-Entität dar, wir ersetzen sie durch &quot; und &amp;.
  3. Die Zeichen ' und & stellen das Ende des Attributwerts und den Anfang einer HTML-Entität dar, wir ersetzen sie durch &apos; und &amp;.

Für jede richtige Antwort erhalten Sie einen Punkt. Natürlich können in allen drei Fällen auch weitere Zeichen durch Entitäten ersetzt werden, das schadet nicht, ist aber nicht notwendig.

Frage Nr. 4

Wir machen weiter. Welche Zeichen müssen bei der Ausgabe der Variablen in diesem Kontext ersetzt werden?

<input value=<?= $str ?>>

Lösung: Wie Sie sehen, fehlen hier Anführungszeichen. Am einfachsten ist es, die Anführungszeichen einfach zu ergänzen und dann genauso zu escapen wie in der vorherigen Frage. Es gibt auch eine zweite Lösung, nämlich Leerzeichen und alle Zeichen, die innerhalb des Tags eine besondere Bedeutung haben, d.h. >, /, = und einige andere, in der Zeichenkette durch HTML-Entitäten zu ersetzen.

Frage Nr. 5

Jetzt wird es interessanter. Welche Zeichen müssen in diesem Kontext behandelt werden:

<script>
	let foo = '<?= $str ?>';
</script>

Lösung: Innerhalb des <script>-Tags bestimmen die Regeln für das Escaping JavaScript. HTML-Entitäten werden hier nicht verwendet, es gilt jedoch eine spezielle Regel. Welche Zeichen escapen wir also? Innerhalb einer JavaScript-Zeichenkette escapen wir natürlich das Zeichen ', das sie begrenzt, und zwar mit einem Backslash, also ersetzen wir es durch \'. Da JavaScript keine mehrzeiligen Zeichenketten unterstützt (nur als Template-Literal), müssen wir auch Zeilenumbruchzeichen escapen. Aber Achtung, neben den üblichen Zeichen \n und \r betrachtet JavaScript auch die Unicode-Zeichen \u2028 und \u2029 als Zeilenumbrüche, die wir ebenfalls escapen müssen. Und schließlich die erwähnte spezielle Regel: In der Zeichenkette darf </script nicht vorkommen. Dies kann z.B. durch Ersetzen durch <\/script verhindert werden.

Wenn Sie das wussten, herzlichen Glückwunsch.

Frage Nr. 6

Der folgende Kontext sieht nur wie eine Variation des vorherigen aus. Was meinen Sie, wird sich die Behandlung unterscheiden?

<p onclick="foo('<?= $str ?>')"></p>

Lösung: Auch hier gelten die Regeln für das Escaping in JavaScript-Zeichenketten, aber im Gegensatz zum vorherigen Kontext, wo nicht mit HTML-Entitäten escaped wurde, wird hier im Gegenteil escaped. Das heißt, wir escapen zuerst die JavaScript-Zeichenkette mit Backslashes und ersetzen dann die Zeichen mit besonderer Bedeutung (" und &) durch HTML-Entitäten. Achtung, die richtige Reihenfolge ist wichtig.

Wie Sie sehen, kann dasselbe JavaScript-Literal im <script>-Element anders kodiert sein als in einem Attribut!

Frage Nr. 7

Kehren wir von JavaScript zurück zu HTML. Welche Zeichen müssen wir innerhalb eines Kommentars ersetzen und auf welche Weise?

<!-- <?= $str ?> -->

Lösung: Innerhalb eines HTML- (und XML-)Kommentars können alle traditionellen Sonderzeichen wie <, &, " und ' vorkommen. Verboten ist, und das wird Sie wahrscheinlich überraschen, das Zeichenpaar --. Das Escaping dieser Sequenz ist nicht spezifiziert, daher liegt es an Ihnen, wie Sie sie ersetzen. Sie können sie mit Leerzeichen durchsetzen. Oder vielleicht durch == ersetzen.

Frage Nr. 8

Wir nähern uns dem Ende, also versuchen wir, die Frage abzuwandeln. Versuchen Sie darüber nachzudenken, worauf bei der Ausgabe der Variablen in diesem Kontext zu achten ist:

<a href="<?= $str ?>">...</a>

Lösung: Neben dem Escaping ist es wichtig zu überprüfen, dass die URL kein gefährliches Schema wie javascript: enthält, da eine so zusammengesetzte URL nach dem Klicken den Code des Angreifers aufrufen würde.

Frage Nr. 9

Zum Abschluss eine Perle für echte Feinschmecker. Es handelt sich um ein Beispiel einer Anwendung, die ein modernes JavaScript-Framework verwendet, konkret Vue. Mal sehen, ob Ihnen einfällt, worauf bei der Ausgabe der Variablen innerhalb des Elements #app zu achten ist:

<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>

Dieser Code erstellt eine Vue-Anwendung, die in das Element #app gerendert wird. Vue versteht den Inhalt dieses Elements als sein Template. Und innerhalb des Templates interpretiert es doppelte geschweifte Klammern, die die Ausgabe einer Variablen oder den Aufruf von JavaScript-Code bedeuten (z.B. {{ foo }}).

Das heißt, innerhalb des Elements #app haben neben den Zeichen < und & auch das Paar {{ eine besondere Bedeutung, das wir durch eine andere entsprechende Sequenz ersetzen müssen, damit Vue es nicht als sein Tag interpretiert. Das Ersetzen durch HTML-Entitäten hilft in diesem Fall jedoch nicht. Wie geht man damit um? Ein Trick funktioniert: Zwischen die Klammern fügen wir einen leeren HTML-Kommentar {<!-- -->{ ein, und Vue ignoriert eine solche Sequenz.

Quiz-Ergebnisse

Wie haben Sie im Quiz abgeschnitten? Wie viele richtige Antworten haben Sie? Wenn Sie mindestens 4 Fragen richtig beantwortet haben, gehören Sie zu den besten 8 % der Löser – herzlichen Glückwunsch!

Um jedoch die Sicherheit Ihrer Website zu gewährleisten, ist es unerlässlich, die Ausgabe in allen Situationen korrekt zu behandeln.

Wenn Sie überrascht waren, wie viele verschiedene Kontexte auf einer normalen HTML-Seite auftreten können, dann wissen Sie, dass wir bei weitem nicht alle erwähnt haben. Das wäre ein viel längeres Quiz gewesen. Dennoch müssen Sie kein Experte für das Escaping in jedem Kontext sein, wenn Ihr Template-System dies beherrscht.

Lassen Sie uns sie also ausprobieren.

Wie schneiden Template-Systeme ab?

Alle modernen Template-Systeme rühmen sich der Funktion Autoescaping, die automatisch alle ausgegebenen Variablen escaped. Wenn sie dies korrekt tun, ist Ihre Website sicher. Wenn sie es falsch machen, ist die Website dem Risiko einer XSS-Schwachstelle mit all ihren schwerwiegenden Folgen ausgesetzt.

Wir testen beliebte Template-Systeme anhand der Fragen dieses Quiz, um herauszufinden, wie effektiv ihr Autoescaping ist. Der dTest der Template-Systeme für PHP beginnt.

Twig ❌

Als erstes kommt das Template-System Twig (Version 3.5) an die Reihe, das am häufigsten in Verbindung mit dem Symfony-Framework verwendet wird. Wir geben ihm die Aufgabe, alle Quizfragen zu beantworten. Die Variable $str wird immer mit einer kniffligen Zeichenkette gefüllt, und wir schauen uns an, wie es mit ihrer Ausgabe umgeht. Die Ergebnisse sehen Sie rechts. Sie können seine Antworten und sein Verhalten auch im Playground untersuchen.

   {% 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 ist in sechs von neun Tests durchgefallen!

Leider funktioniert das automatische Escaping von Twig nur in HTML-Text und Attributen, und das auch nur, wenn sie in Anführungszeichen eingeschlossen sind. Sobald Anführungszeichen fehlen, meldet Twig keinen Fehler und erzeugt eine XSS-Sicherheitslücke.

Dies ist besonders ärgerlich, da Attributwerte in populären Bibliotheken wie React oder Svelte so geschrieben werden. Ein Programmierer, der gleichzeitig Twig und React verwendet, kann daher ganz natürlich die Anführungszeichen vergessen.

Das Autoescaping von Twig versagt auch in allen anderen Beispielen. In den Kontexten (5) und (6) muss manuell mit {{ str|escape('js') }} escaped werden, für andere Kontexte bietet Twig keine Escaping-Funktion an. Es verfügt auch nicht über einen Schutz vor der Ausgabe eines schädlichen Links (8) oder Unterstützung für Templates für Vue (9).

Blade ❌❌

Der zweite Teilnehmer ist das Template-System Blade (Version 10.9), das eng mit Laravel und seinem Ökosystem integriert ist. Wir überprüfen erneut seine Fähigkeiten anhand unserer Quizfragen. Seine Antworten können Sie auch im Playground untersuchen.

   @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 ist in sechs von neun Tests durchgefallen!

Das Ergebnis ähnelt dem von Twig. Es gilt wieder, dass das automatische Escaping nur in HTML-Text und Attributen funktioniert und nur, wenn sie in Anführungszeichen eingeschlossen sind. Das Autoescaping von Blade versagt auch in allen anderen Beispielen. In den Kontexten (5) und (6) muss manuell mit {{ Js::from($str) }} escaped werden. Für andere Kontexte bietet Blade keine Escaping-Funktion an. Es verfügt weder über einen Schutz vor der Ausgabe eines schädlichen Links (8) noch über Unterstützung für Templates für Vue (9).

Was jedoch überraschend ist, ist das Versagen der @php-Direktive in Blade, was dazu führt, dass eigener PHP-Code direkt in die Ausgabe geschrieben wird, wie Sie in der letzten Zeile sehen.

Smarty ❌❌❌

Nun testen wir das älteste Template-System für PHP, nämlich Smarty (Version 4.3). Zur großen Überraschung hat dieses System kein aktives automatisches Escaping. Sie müssen also beim Ausgeben von Variablen entweder jedes Mal den Filter {$var|escape} angeben oder das automatische HTML-Escaping aktivieren. Die Information dazu ist in der Dokumentation ziemlich versteckt.

   {$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 ist in sechs von neun Tests durchgefallen!

Das Ergebnis ähnelt auf den ersten Blick dem der vorherigen Bibliotheken. Smarty kann automatisch nur in HTML-Text und Attributen escapen, und das auch nur, wenn die Werte in Anführungszeichen eingeschlossen sind. Überall sonst versagt es. In den Kontexten (5) und (6) muss manuell mit {$str|escape:javascript} escaped werden. Dies ist jedoch nur möglich, wenn das automatische HTML-Escaping nicht aktiv ist, da sich diese Escapings sonst gegenseitig stören. Smarty ist somit aus Sicherheitssicht der absolute Verlierer dieses Tests.

Latte ✅

Das Trio schließt das Template-System Latte (Version 3.0) ab. Wir testen sein Autoescaping. Seine Antworten und sein Verhalten können Sie ebenfalls im Playground untersuchen.

   {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 hat in allen neun Aufgaben exzellent abgeschnitten!

Es kam mit fehlenden Anführungszeichen bei HTML-Attributen zurecht, verarbeitete JavaScript sowohl im <script>-Element als auch in Attributen und konnte auch mit der verbotenen Sequenz in HTML-Kommentaren umgehen.

Mehr noch, es verhinderte die Situation, dass ein Klick auf einen vom Angreifer eingeschleusten Link dessen Code ausführen könnte. Und es kam mit dem Escaping von Tags für Vue zurecht.

Bonus-Test

Eine der wesentlichen Fähigkeiten aller Template-Systeme ist die Arbeit mit Blöcken und die damit verbundene Template-Vererbung. Wir versuchen daher, allen getesteten Template-Systemen noch eine Aufgabe zu geben. Wir erstellen einen Block description, den wir in einem HTML-Attribut ausgeben. In der realen Welt würde sich die Definition des Blocks natürlich im Child-Template befinden und seine Ausgabe im Parent-Template, also zum Beispiel im Layout. Dies ist nur eine vereinfachte Form, reicht aber aus, um das Autoescaping beim Ausgeben von Blöcken zu testen. Wie haben sie abgeschnitten?

Twig: durchgefallen ❌ behandelt Zeichen beim Ausgeben von Blöcken nicht

{% block description %}
	rock n' roll
{% endblock %}

<meta name='description'
	content='{{ block('description') }}'>




<meta name='description'
	content=' rock n' roll '> ❌

Blade: durchgefallen ❌ behandelt Zeichen beim Ausgeben von Blöcken nicht

@section('description')
	rock n' roll
@endsection

<meta name='description'
	content='@yield('description')'>




<meta name='description'
	content=' rock n' roll '> ❌

Latte: bestanden ✅ hat problematische Zeichen beim Ausgeben von Blöcken korrekt behandelt

{block description}
	rock n' roll
{/block}

<meta name='description'
	content='{include description}'>




<meta name='description'
	content=' rock n&apos; roll '> ✅

Warum sind so viele Websites anfällig?

Autoescaping in Systemen wie Twig, Blade oder Smarty funktioniert so, dass es einfach fünf Zeichen <>"'& durch HTML-Entitäten ersetzt und den Kontext nicht unterscheidet. Daher funktioniert es nur in einigen Situationen und versagt in allen anderen. Naives Autoescaping ist eine gefährliche Funktion, da es ein falsches Gefühl der Sicherheit erzeugt.

Es ist daher nicht überraschend, dass derzeit mehr als 27 % der Websites kritische Schwachstellen aufweisen, hauptsächlich XSS (Quelle: Acunetix Web Vulnerability Report). Wie kommt man da raus? Verwenden Sie ein Template-System, das Kontexte unterscheidet.

Latte ist das einzige Template-System in PHP, das ein Template nicht nur als Zeichenkette betrachtet, sondern HTML versteht. Es versteht, was Tags, Attribute usw. sind. Es unterscheidet Kontexte. Und deshalb escaped es korrekt im HTML-Text, anders innerhalb eines HTML-Tags, anders innerhalb von JavaScript usw.

Latte stellt somit das einzige sichere Template-System dar.


Darüber hinaus bietet es dank seines Verständnisses von HTML die wunderbaren n:Attribute, die die Benutzer lieben:

<ul n:if="$menu">
	<li n:foreach="$menu->getItems() as $item">{$item->title}</li>
</ul>