Latte Is Synonymous with Safety
Latte is the only PHP templating system with effective protection against the critical Cross-site Scripting (XSS) vulnerability. This is thanks to the so-called context-sensitive escaping. Let's talk,
- what is the principle of the XSS vulnerability and why is it so dangerous
- what makes Latte so effective in defending against XSS
- why Twig, Blade and other templates can be easily compromised
Cross-Site Scripting (XSS)
Cross-site Scripting (XSS for short) is one of the most common vulnerabilities in websites and a very dangerous one at that. It allows an attacker to insert a malicious script (called malware) into a foreign site that executes in the browser of an unsuspecting user.
What can such a script do? For example, it can send arbitrary content from the compromised site to the attacker, including sensitive data displayed after login. It can modify the page or make other requests on behalf of the user. For example, if it were webmail, it could read sensitive messages, modify the displayed content, or change settings, e.g., turn on forwarding copies of all messages to the attacker's address to gain access to future emails.
This is also why XSS tops the list of the most dangerous vulnerabilities. If a vulnerability is discovered on a website, it should be removed as soon as possible to prevent exploitation.
How Does the Vulnerability Arise?
The error occurs in the place where the web page is generated and the variables are printed. Imagine that you are creating a search page, and at the beginning there will be a paragraph with the search term in the form:
echo '<p>Search results for <em>' . $search . '</em></p>';
An attacker can write any string, including HTML code like <script>alert("Hacked!")</script>
, into the
search field and thus into the $search
variable. Since the output is not sanitized in any way, it becomes part of the
displayed page:
<p>Search results for <em><script>alert("Hacked!")</script></em></p>
Instead of outputting the search string, the browser executes JavaScript. And thus the attacker takes over the page.
You might argue that putting code into a variable will indeed execute JavaScript, but only in the attacker's browser. How does it get to the victim? From this perspective, we can distinguish several types of XSS. In our search page example, we are talking about reflected XSS. In this case, the victim needs to be tricked into clicking on a link that contains malicious code in the parameter:
https://example.com/?search=<script>alert("Hacked!")</script>
Although it requires some social engineering to make the user to access the link, it's not difficult. Users click on links,
whether in emails or on social media, without much thought. And the fact that there's something suspicious in the address can be
masked by URL shortener, so the user only sees bit.ly/xxx
.
However, there is a second and much more dangerous form of attack known as stored XSS or persistent XSS, where an attacker manages to store malicious code on the server so that it is automatically inserted into certain pages.
An example of this is websites where users post comments. An attacker sends a post containing code and it is saved on the server. If the site is not secure enough, it will then run in every visitor's browser.
It would seem that the point of the attack is to get the <script>
string into the page. In fact, there are many ways to embed
JavaScript. Let's take an example of embedding using an HTML attribute. Let's have a photo gallery where you can insert a
caption to the images, which is printed in the alt
attribute:
echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';
An attacker just needs to insert a cleverly constructed string " onload="alert('Hacked!')
as a label, and if the
output is not sanitized, the resulting code will look like this:
<img src="photo0145.webp" alt="" onload="alert('Hacked!')">
The fake onload
attribute now becomes part of the page. The browser will execute the code it contains as soon as
the image is downloaded. Hacked!
How to Defend Against XSS?
Any attempts to detect an attack using a blacklist, such as blocking the <script>
string, etc. are
insufficient. The basis of a workable defense is consistent sanitization of all data printed inside the page.
First of all, this involves replacing all characters with special meaning with other matching sequences, which is called
escaping in slang (the first character of the sequence is called the escape character, hence the name). For example, in
HTML text, the character <
has a special meaning, which, if it is not to be interpreted as the beginning of a tag,
must be replaced by a visually corresponding sequence, the so-called HTML entity <
. And the browser prints a
character.
It is very important to distinguish the context in which the data is output. Because different contexts sanitize strings differently. Different characters have special meaning in different contexts. For example, escaping in HTML text, in HTML attributes, inside some special elements, etc. is different. We'll discuss this in detail in a moment.
It's best to perform escaping directly when the string is written out in the page, ensuring that it is actually done, and done just once. It is best if the processing is handled automatically directly by the templating system. Because if the treatment is not done automatically, the programmer may forget about it. And one omission means the site is vulnerable.
However, XSS doesn't just affect the output of data in templates, but also other parts of the application that must properly
handle untrusted data. For example, JavaScript in your application must not use innerHTML
in conjunction with them,
but only innerText
or textContent
. Special care should be taken with functions that evaluate strings
like JavaScript, which is eval()
, but also setTimeout()
, or using setAttribute()
with event
attributes like onload
, etc. But this goes beyond the scope covered by templates.
The ideal 3-point defense:
- Recognize the context in which the data is being output
- sanitizes the data according to the rules of that context (i.e. „context-aware“)
- does this automatically
Context-Aware Escaping
What exactly is meant by the word context? It is a place in the document with its own rules for treating the data to be output. It depends on the type of document (HTML, XML, CSS, JavaScript, plain text, …) and may vary in specific parts of the document. For example, in an HTML document, there are many such places (contexts) where very different rules apply. You might be surprised how many there are. Here are the first four:
<p>#text</p>
<img src="#attribute">
<textarea>#rawtext</textarea>
<!-- #comment -->
The initial and basic context of an HTML page is HTML text. What are the rules here? The special meaning characters
<
and &
represent the beginning of a tag or entity, so we have to escape them by replacing them
with the HTML entity (<
with <
, &
with &
).
The second most common context is the value of an HTML attribute. It differs from text in that the special meaning here goes to
the quotation mark "
or '
that delimits the attribute. This needs to be written as an entity so that it
is not seen as the end of the attribute. On the other hand, the <
character can be safely used in an attribute
because it has no special meaning here; it cannot be understood as the beginning of a tag or comment. But beware, in HTML you can
write attribute values without quotes, in which case a whole range of characters have special meaning, so it is another separate
context.
It may surprise you, but special rules apply inside the <textarea>
and <title>
elements,
where the <
character need not (but can) be escaped unless followed by /
. But that's more of a
curiosity.
It's interesting inside the HTML comments. Here, HTML entities are not used for escaping. There is no specification even stating how to escaping in comments. You just have to follow the somewhat curious rules and avoid certain character combinations in them.
Contexts can also be layered, which happens when we embed JavaScript or CSS into HTML. This can be done in two different ways, by element or attribute:
<script>#js-element</script>
<img onclick="#js-attribute">
<style>#css-element</style>
<p style="#css-attribute"></p>
Two ways and two different kinds of escaping data. Within the <script>
and <style>
elements, as in the case of HTML comments, escaping using HTML entities is not performed. When escaping data inside these
elements, there is only one rule: the text must not contain the sequence </script
and </style
respectively.
On the other hand, the style
and on***
attributes are escaped using HTML entities.
And, of course, inside embedded JavaScript or CSS, the escaping rules of those languages apply. So a string in an attribute
such as onload
is escaped first according to JS rules and then according to HTML attribute rules.
Ugh… As you can see, HTML is a very complex document with layers of contexts, and without knowing exactly where I'm outputting the data (i.e. in what context), there's no telling how to do it right.
Do You Want an Example?
Let's have a string Rock'n'Roll
.
If you output it in HTML text, you don't need to make any substitutions in this case, because the string doesn't contain any character with special meaning. The situation is different if you write it inside an HTML attribute enclosed in single quotes. In this case, you need to escaping the quotes to HTML entities:
<div title='Rock'n'Roll'></div>
This was easy. A much more interesting situation occurs when the context is layered, for example if the string is part of JavaScript.
So first we write it out into the JavaScript itself. That is, we wrap it in quotes and at the same time escaping the quotes
contained in it using the \
character:
'Rock\'n\'Roll'
We can add a function call to make the code do something:
alert('Rock\'n\'Roll');
If we insert this code into an HTML document using <script>
, we don't need to modify anything else, because
the forbidden </script
sequence is not present:
<script> alert('Rock\'n\'Roll'); </script>
However, if we want to insert it into an HTML attribute, we still need to escaping quotes to HTML entities:
<div onclick='alert('Rock\'n\'Roll')'></div>
However, nested context doesn't have to be just JS or CSS. It is also commonly a URL. Parameters in URLs are escaped by
converting special characters to sequences starting with %
. Example:
https://example.org/?a=Jazz&b=Rock%27n%27Roll
And when we output this string in an attribute, we still apply escaping according to this context and replace
&
with &
:
<a href="https://example.org/?a=Jazz&b=Rock%27n%27Roll">
If you've read this far, congratulations, it's been exhausting. Now you have a good idea of what contexts and escaping are. And you don't have to worry about it being complicated. Latte does that for you automatically.
Latte vs Naive Systems
We've shown how to properly escaping in an HTML document and how crucial it is to know the context, i.e., where you're outputting the data. In other words, how context sensitive escaping works. While this is a prerequisite for functional XSS defense, Latte is the only templating system for PHP that does this.
How is this possible when all systems today claim to have automatic escaping? Automatic escaping without knowing the context is a bit of bullshit that creates a false sense of security.
Templating systems like Twig, Laravel Blade and others don't see any HTML structure in the template. Therefore, they don't see contexts either. Compared to Latte, they are blind and naive. They only handle their own markup, everything else is an irrelevant character stream to them:
░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░
░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░{{ text }}░░░░
- in text: <span>{{ text }}</span>
- in tag: <span {{ text }} ></span>
- in attribute: <span title='{{ text }}'></span>
- in unquoted attribute: <span title={{ text }}></span>
- in attribute containing URL: <a href="{{ text }}"></a>
- in attribute containing JavaScript: <img onload="{{ text }}">
- in attribute containing CSS: <span style="{{ text }}"></span>
- in JavaScriptu: <script>var = {{ text }}</script>
- in CSS: <style>body { content: {{ text }}; }</style>
- in comment: <!-- {{ text }} -->
Naive systems just mechanically convert < > & ' "
characters to HTML entities, which is a valid way of
escaping in most uses, but far from always. Thus, they cannot detect or prevent various security holes, as we will
show below.
Latte sees the template the same way you do. It understands HTML, XML, recognizes tags, attributes, etc. And because of this, it distinguishes between contexts and treats data accordingly. So it offers really effective protection against the critical Cross-site Scripting vulnerability.
Live Demonstration
On the left you can see the template in Latte, on the right is the generated HTML code. The $text
variable is
output several times, each time in a slightly different context. And therefore escaped a bit differently. You can edit the
template code yourself, for example change the content of the variable etc. Try it:
Isn't that great! Latte does context-sensitive escaping automatically, so the programmer:
- doesn't have to think or know how to escape data
- can't be wrong
- can't forget about it
These aren't even all the contexts that Latte distinguishes when outputting and for which it customizes data treatment. We'll go through more interesting cases now.
How to Hack Naive Systems
We will use a few practical examples to show how important context differentiation is and why naive templating systems do not provide sufficient protection against XSS, unlike Latte. We will use Twig as a representative of a naive system in the examples, but the same applies to other systems.
Attribute Vulnerability
Let's try to inject malicious code into the page using the HTML attribute as we showed above. Let's have a template in Twig displaying an image:
<img src={{ imageFile }} alt={{ imageAlt }}>
Note that there are no quotes around the attribute values. The coder may have forgotten them, which just happens. For example, in React, the code is written like this, without quotes, and a coder who is switching languages can easily forget about the quotes.
The attacker inserts a cleverly constructed string foo onload=alert('Hacked!')
as the image caption. We already
know that Twig can't tell if a variable is being printed in a stream of HTML text, inside an attribute, inside an HTML comment,
etc.; in short, it doesn't distinguish between contexts. And it just mechanically converts < > & ' "
characters to HTML entities. So the resulting code will look like this:
<img src=photo0145.webp alt=foo onload=alert('Hacked!')>
A security hole has been created!
A fake onload
attribute has become part of the page and the browser executes it immediately after downloading
the image.
Now let's see how Latte handles the same template:
<img src={$imageFile} alt={$imageAlt}>
Latte sees the template the same way you do. Unlike Twig, it understands HTML and knows that a variable is printed as an attribute value that is not in quotes. That's why it adds them. When an attacker inserts the same caption, the resulting code will look like this:
<img src="photo0145.webp" alt="foo onload=alert('Hacked!')">
Latte successfully prevented XSS.
Printing a Variable in JavaScript
Thanks to context-sensitive escaping, it is possible to use PHP variables natively inside JavaScript.
<p onclick="alert({$movie})">{$movie}</p>
<script>var movie = {$movie};</script>
If $movie
variable stores 'Amarcord & 8 1/2'
string it generates the following output. Notice
different escaping used in HTML and JavaScript and also in onclick
attribute:
<p onclick="alert("Amarcord & 8 1\/2")">Amarcord & 8 1/2</p>
<script>var movie = "Amarcord & 8 1\/2";</script>
Link Checking
Latte automatically checks whether the variable used in the src
or href
attributes contains a web URL
(ie protocol HTTP) and prevents the writing of links that may pose a security risk.
{var $link = 'javascript:attack()'}
<a href={$link}>click here</a>
Writes:
<a href="">click here</a>
The check can be turned off using a filter nocheck.
Limits of Latte
Latte is not a complete XSS protection for the entire application. We would be unhappy if you stopped to think about security when using Latte. The goal of Latte is to ensure that an attacker cannot alter the structure of a page, tamper with HTML elements or attributes. But it does not check the content correctness of the data being output. Or the correctness of JavaScript behavior. That's beyond the scope of the templating system. Verifying the correctness of data, especially those entered by the user and thus untrusted, is an important task for the programmer.