Quiz: Can you defend against XSS vulnerability?
Test your knowledge and security skills in this quiz! Can you prevent an attacker from taking control of an HTML page?
In all tasks, you will address the same question: how to properly display the
variable $str
in an HTML page without creating an XSS vulnerability. The basis
of defense is escaping, which means replacing characters with special
meanings with corresponding sequences. For example, when outputting a string to
HTML text, in which the character <
has a special meaning
(indicating the beginning of a tag), we replace it with the HTML entity
<
, and the browser correctly displays the symbol
<
.
Be vigilant, as XSS vulnerability is very serious. It can cause an attacker to take control of a page or even a user's account. Good luck and may you succeed in keeping the HTML page safe!
The first trio of questions
Specify which characters need to be handled and how in the first, second, and third examples:
1) <p><?= $str ?></p>
2) <input value="<?= $str ?>">
3) <input value='<?= $str ?>'>
If the output was not treated in any way, it would become part of the
displayed page. If an attacker managed to insert string
'foo" onclick="evilCode()'
into a variable and the output was not
treated, it would cause their code to be executed when clicking on the
element:
$str = 'foo" onclick="evilCode()'
❌ not treated: <input value="foo" onclick="evilCode()">
✅ treated: <input value="foo" onclick="evilCode()">
Solutions for each example:
- characters
<
and&
represent the beginning of an HTML tag and entity; replace them with<
and&
- characters
"
and&
represent the end of an attribute value and the beginning of an HTML entity; replace them with"
and&
- characters
'
and&
represent the end of an attribute value and the beginning of an HTML entity; replace them with'
and&
You get a point for each correct answer. Of course, in all three cases, you can replace other characters with entities as well; it doesn't cause any harm, but it is not necessary.
Question No. 4
Moving on, which characters need to be replaced when displaying a variable in this context?
<input value=<?= $str ?>>
Solution: As you can see, the quotes are missing here. The easiest way is to
simply add the quotes and then escape as in the previous question. There is also
a second solution, which is to replace spaces and all characters that have
special meaning inside a tag, such as >
, /
,
=
, and some
others with HTML entities.
Question No. 5
Now it's getting more interesting. Which characters need to be treated in this context:
<script>
let foo = '<?= $str ?>';
</script>
Solution: Inside the <script>
tag, the escaping rules are
determined by JavaScript. HTML entities are not used here, but there is one
special rule. So which characters do we escape? Inside the JavaScript string, we
naturally escape the '
character that delimits it, using a
backslash, replacing it with \'
. Since JavaScript doesn't support
multi-line strings (except as template
literals), we also need to escape newline characters. However, be aware that
in addition to the usual \n
and \r
characters,
JavaScript also considers the Unicode characters \u2028
and
\u2029
as newline characters, which we must escape as well.
Finally, the mentioned special rule: the string must not contain
</script
. This can be prevented, for example, by replacing it
with <\/script
.
If you knew this, congratulations.
Question No. 6
The following context seems to be just a variation of the previous one. Do you think the treatment will be different?
<p onclick="foo('<?= $str ?>')"></p>
Solution: Again, the escaping rules for JavaScript strings apply here, but
unlike the previous context where HTML entities were not escaped, here they are
escaped. So first, we escape the JavaScript string using backslashes and then
replace the special characters ("
and &
) with HTML
entities. Be careful, the correct order is important.
As you can see, the same JavaScript literal can be encoded differently in a
<script>
element and differently in an attribute!
Question No. 7
Let's return from JavaScript back to HTML. Which characters do we need to replace inside the comment and how?
<!-- <?= $str ?> -->
Solution: Inside an HTML (and XML) comment, all traditional special
characters, such as <
, &
, "
and
'
, can appear. What is forbidden, and this may surprise you, is the
pair of characters --
. Escaping this sequence is not specified, so
it is up to you how to replace it. You can intersperse them with spaces. Or, for
example, replace them with ==
.
Question No. 8
We are approaching the end, so let's try to vary the question. Try to think about what you need to be careful about when printing a variable in this context:
<a href="<?= $str ?>">...</a>
Solution: In addition to escaping, it is important to also verify that the
URL does not contain a dangerous scheme like javascript:
, because a
URL composed like this would execute the attacker's code when clicked.
Question No. 9
Finally, a treat for real connoisseurs. This is an example of an application
using a modern JavaScript framework, specifically Vue. Let's see if you can
figure out what to be careful about when printing a variable inside the
#app
element:
<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>
This code creates a Vue application that will be rendered into the
#app
element. Vue interprets the content of this element as its
template. And within the template, it interprets
double curly braces, which represent variable output or calling JavaScript code
(e.g., {{ foo }}
).
So, within the #app
element, besides the characters
<
and &
, the pair of {{
also has a
special meaning, which we need to replace with another appropriate sequence to
prevent Vue from interpreting it as its own tag. Replacing with HTML entities
doesn't help in this case. How to deal with it? There is a trick: insert an
empty HTML comment between the braces {<!-- -->{
, and Vue
ignores this sequence.
Quiz Results
How did you do in the quiz? How many correct answers do you have? If you answered at least 4 questions correctly, you are among the top 8% of solvers – congratulations!
However, ensuring the security of your website requires properly handling output in all situations.
If you were surprised by how many different contexts can appear on a typical HTML page, know that we haven't mentioned all of them by far. That would make the quiz much longer. Nevertheless, you don't have to be an expert in escaping in every context if your templating system can handle it.
So, let's test them.
How do templating systems perform?
All modern templating systems boast an autoescaping feature that automatically escapes all outputted variables. If they do it correctly, your website is safe. If they do it poorly, the site is exposed to the risk of XSS vulnerability with all its serious consequences.
We will test popular templating systems from the questions in this quiz to determine the effectiveness of their auto-escaping. Let the review of PHP templating systems begin.
Twig ❌
First up is the Twig templating
system (version 3.5), most commonly used in conjunction with the Symfony
framework. We'll task it with answering all the quiz questions. The variable
$str
will always be filled with a tricky string, and we'll see how
it handles its output. You can see the results on the right. You can also
explore its answers and behavior on the playground.
{% 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 failed in six out of nine tests!
Unfortunately, Twig's automatic escaping works only in HTML text and attributes, and even then only when they are enclosed in quotes. As soon as the quotes are missing, Twig does not report any error and creates an XSS security hole.
This is particularly unpleasant because this is how attribute values are written in popular libraries like React or Svelte. A programmer who uses both Twig and React can quite naturally forget about the quotes.
Twig's autoescaping also fails in all other examples. In contexts (5) and
(6), manual escaping is needed using {{ str|escape('js') }}
, while
for other contexts, Twig does not even offer an escaping function. It also lacks
protection against printing a malicious link (8) or support for Vue
templates (9).
Blade ❌❌
The second participant is the Blade templating system (version 10.9), which is tightly integrated with Laravel and its ecosystem. Again, we will test its abilities on our quiz questions. You can also explore its answers on the playground.
@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 failed in six out of nine tests!
The result is similar to Twig. Again, automatic escaping works only in HTML
text and attributes and only if they are enclosed in quotes.
Blade's autoescaping also fails in all other examples. In contexts (5) and (6),
manual escaping is needed using {{ Js::from($str) }}
. For other
contexts, Blade does not even offer an escaping function. It also lacks
protection against printing a malicious link (8) or support for Vue
templates (9).
However, what is surprising is the failure of the @php
directive
in Blade, which causes the output of its own PHP code directly to the output, as
seen in the last line.
Smarty ❌❌❌
Now, let's test the oldest templating system for PHP, which is Smarty (version 4.3). To great surprise, this
system does not have active automatic escaping. Thus, when printing variables,
you either have to specify the filter {$var|escape}
every time, or
activate automatic HTML escaping. Information about this is somewhat buried in
the documentation.
{$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 failed in six out of nine tests!
At first glance, the result is similar to the previous libraries. Smarty can
only automatically escape in HTML text and attributes, and only when the values
are enclosed in quotes. It fails everywhere else. In contexts (5) and (6), you
need to manually escape using {$str|escape:javascript}
. However,
this is only possible when automatic HTML escaping is not active, as these
escape methods conflict with each other. From a security perspective, Smarty is
a complete failure in this test.
Latte ✅
The trio is concluded by the Latte templating system (version 3.0). We will test its autoescaping. You can also explore its answers and behavior on the playground.
{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 excelled in all nine tasks!
It managed to handle missing quotes in HTML attributes, processed JavaScript
both in the <script>
element and in attributes, and dealt
with the forbidden sequence in HTML comments.
What's more, it prevented a situation where clicking on a malicious link provided by an attacker could execute their code. And it managed to handle the escaping of tags for Vue.
Bonus test
One of the essential capabilities of all templating systems is working with
blocks and the related template inheritance. Therefore, we will give all tested
templating systems one more task. We will create a description
block, which we will print in an HTML attribute. In the real world, the block
definition would, of course, be located in the child template and its output in
the parent template, such as the layout. This is just a simplified form, but it
is enough to test the autoescaping when outputting blocks. How did they
perform?
Twig: failed ❌ when outputting blocks, characters are not properly escaped
{% block description %}
rock n' roll
{% endblock %}
<meta name='description'
content='{{ block('description') }}'>
<meta name='description'
content=' rock n' roll '> ❌
Blade: failed ❌ when outputting blocks, characters are not properly escaped
@section('description')
rock n' roll
@endsection
<meta name='description'
content='@yield('description')'>
<meta name='description'
content=' rock n' roll '> ❌
Latte: passed ✅ when outputting blocks, it correctly handled problematic characters
{block description}
rock n' roll
{/block}
<meta name='description'
content='{include description}'>
<meta name='description'
content=' rock n' roll '> ✅
Why are so many websites vulnerable?
Autoescaping in systems like Twig, Blade, or Smarty works by simply replacing
five characters <>"'&
with HTML entities and does not
distinguish context. Therefore, it only works in some situations and fails in
all others. Naive autoescaping is a dangerous feature because it creates a
false sense of security.
It is not surprising, then, that currently more than 27% of websites have critical vulnerabilities, mainly XSS (source: Acunetix Web Vulnerability Report). How to get out of this situation? Use a templating system that distinguishes contexts.
Latte is the only PHP templating system that does not perceive a template as just a string of characters but understands HTML. It knows what tags, attributes, etc., are. It distinguishes contexts. And therefore, it correctly escapes in HTML text, differently inside HTML tags, differently inside JavaScript, etc.
Latte thus represents the only secure templating system.
Moreover, thanks to its understanding of HTML, it offers the wonderful n:attributes, which users love:
<ul n:if="$menu">
<li n:foreach="$menu->getItems() as $item">{$item->title}</li>
</ul>
Sign in to submit a comment