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 context-aware escaping. We will discuss:
- the principle of the XSS vulnerability and why it is so dangerous
- what makes Latte so effective in defending against XSS
- how security holes can easily be created in Twig, Blade, and similar templates
Cross-site Scripting (XSS)
Cross-site Scripting (XSS for short) is one of the most common and dangerous vulnerabilities in websites. It allows an attacker to inject a malicious script (malware) into someone else's page, which then runs in the browser of an unsuspecting user.
What can such a script do? For example, it can send any content from the compromised page to the attacker, including sensitive data displayed after login. It can modify the page or perform other requests on behalf of the user. If it were webmail, for instance, it could read sensitive messages, modify the displayed content, or reconfigure settings, e.g., enable forwarding copies of all messages to the attacker's address to gain access to future emails.
This is why XSS consistently ranks among the most dangerous vulnerabilities. If a vulnerability appears on a website, it must be removed as soon as possible to prevent exploitation.
How Does the Vulnerability Arise?
The error occurs where the web page is generated and variables are printed. Imagine creating a search page, where the beginning includes a paragraph with the searched term like this:
echo '<p>Search results for <em>' . $search . '</em></p>';
An attacker can enter any string into the search box, and thus into the $search
variable, including HTML code like
<script>alert("Hacked!")</script>
. Since the output is not sanitized, it becomes part of the
displayed page:
<p>Search results for <em><script>alert("Hacked!")</script></em></p>
Instead of displaying the search string, the browser executes the JavaScript. And thus, the attacker takes control of the page.
You might argue that inserting code into a variable executes JavaScript, but only in the attacker's browser. How does it reach the victim? From this perspective, we distinguish several types of XSS. In our search example, we are talking about reflected XSS. Here, the victim needs to be tricked into clicking a link containing the malicious code in a parameter:
https://example.com/?search=<script>alert("Hacked!")</script>
Guiding the user to click the link requires some social engineering, but it's not overly complicated. Users click on links,
whether in emails or on social media, without much thought. The fact that the address contains something suspicious can be masked
using a URL shortener; the user then only sees bit.ly/xxx
.
However, there is a second, much more dangerous form of attack known as stored XSS or persistent XSS, where the attacker manages to save malicious code on the server so that it is automatically inserted into certain pages.
An example is pages where users write comments. An attacker submits a post containing code, and it gets saved on the server. If the pages are not sufficiently secured, the code will then run in the browser of every visitor.
It might seem that the core of the attack lies in getting the string <script>
into the page. In reality, there are many ways to insert
JavaScript. Let's show an example using an HTML attribute. Consider a photo gallery where captions can be added to images,
which are displayed in the alt
attribute:
echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';
An attacker simply needs to insert a cleverly crafted string " onload="alert('Hacked!')
as the caption, and if the
output is not sanitized, the resulting code will look like this:
<img src="photo0145.webp" alt="" onload="alert('Hacked!')">
The injected onload
attribute now becomes part of the page. The browser executes the code contained within it as
soon as the image is downloaded. Hacked!
How to Defend Against XSS?
Any attempts to detect attacks using blacklists, such as blocking the string <script>
, are insufficient. The
foundation of a functional defense is consistent sanitization of all data printed within the page.
Primarily, this involves replacing all characters with special meanings with their corresponding sequences, colloquially known
as escaping (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; if it's not meant to be interpreted as the start of a tag, we must
replace it with a visually corresponding sequence, the HTML entity <
. The browser then displays the
less-than sign.
It is crucial to distinguish the context in which data is printed. Because strings are sanitized differently in different contexts. Different characters have special meanings in different contexts. For example, escaping differs in HTML text, HTML attributes, inside certain special elements, etc. We will discuss this in detail shortly.
Sanitization is best performed right when the string is printed on the page, ensuring it is actually done and done exactly once. It's best if the sanitization is handled automatically by the templating system itself. Because if sanitization is not automatic, the programmer might forget it. And a single oversight means the website is vulnerable.
However, XSS is not only about printing data in templates but also concerns other parts of the application that must handle
untrusted data correctly. For instance, JavaScript in your application must use innerText
or textContent
in connection with untrusted data, not innerHTML
. Special attention must be paid to functions that evaluate strings
as JavaScript, such as eval()
, but also setTimeout()
, or the use of setAttribute()
with
event attributes like onload
, etc. This, however, goes beyond the scope covered by templates.
The ideal defense in 3 points:
- Recognizes the context in which data is being printed.
- Sanitizes 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's a location within the document with its own rules for handling the data being printed. It depends on the document type (HTML, XML, CSS, JavaScript, plain text, …) and can differ in specific parts. For example, in an HTML document, there are many 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 default and basic context of an HTML page is HTML text. What are the rules here? The characters <
and
&
have special meanings, representing the start of a tag or entity, so we must escape them by replacing them with
HTML entities (<
becomes <
, &
becomes &
).
The second most common context is the value of an HTML attribute. It differs from text in that the quotation mark
"
or '
, which delimits the attribute, has a special meaning here. It needs to be written as an entity so
it's not interpreted as the end of the attribute. Conversely, the <
character can be used safely within an
attribute because it has no special meaning there; it cannot be interpreted as the start of a tag or comment. But beware, in HTML,
attribute values can also be written without quotes, in which case a whole range of characters have special meanings, making it
another separate context.
You might be surprised, but special rules apply inside the <textarea>
and <title>
elements, where the <
character doesn't need to be (but can be) escaped unless followed by /
. But
that's more of a minor detail.
It gets interesting inside HTML comments. Here, HTML entities are not used for escaping. In fact, no specification states how escaping should be done in comments. You just need to follow somewhat curious rules and avoid certain character combinations within them.
Contexts can also be layered, which occurs when we embed JavaScript or CSS into HTML. This can be done in two different ways, using an element or an attribute:
<script>#js-element</script>
<img onclick="#js-attribute">
<style>#css-element</style>
<p style="#css-attribute"></p>
Two paths and two different ways of escaping data. Inside the <script>
and <style>
elements, just like with HTML comments, escaping using HTML entities is not performed. When printing data inside these elements,
only one rule needs to be followed: the text must not contain the sequence </script
or </style
,
respectively.
Conversely, in style
and on***
attributes, escaping is done using HTML entities.
And, of course, within the nested JavaScript or CSS, the escaping rules of those languages apply. So, a string in an attribute
like onload
is first escaped according to JS rules and then according to HTML attribute rules.
Phew… As you can see, HTML is a very complex document where contexts are layered, and without realizing exactly where you are printing data (i.e., in which context), you cannot say how to do it correctly.
Want an Example?
Let's take the string Rock'n'Roll
.
If you print it in HTML text, in this particular case, no replacement is needed because the string does not contain any characters with special meaning. The situation changes if you print it inside an HTML attribute enclosed in single quotes. In that case, you need to escape the quotes into HTML entities:
<div title='Rock'n'Roll'></div>
That was simple. A much more interesting situation arises when contexts are layered, for example, if the string is part of JavaScript.
First, let's print it within the JavaScript itself. That is, we wrap it in quotes and simultaneously escape the quotes
contained within it using the \
character:
'Rock\'n\'Roll'
We can also 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>
, no further modification is needed because it
does not contain the forbidden sequence </script
:
<script> alert('Rock\'n\'Roll'); </script>
However, if we wanted to insert it into an HTML attribute, we still need to escape the quotes into HTML entities:
<div onclick='alert('Rock\'n\'Roll')'></div>
But the nested context doesn't have to be just JS or CSS. It is also commonly a URL. Parameters in URLs are escaped by
converting characters with special meanings into sequences starting with %
. Example:
https://example.org/?a=Jazz&b=Rock%27n%27Roll
And when we print 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 was exhaustive. Now you have a good understanding of what contexts and escaping are. And you don't need to worry about it being complicated. Latte does this for you automatically.
Latte vs Naive Systems
We have shown how to properly escape in an HTML document and how crucial the knowledge of context is, i.e., the place where we print data. In other words, how context-aware escaping works. Although it is a necessary prerequisite for a functional defense against XSS, Latte is the only PHP templating system that can do this.
How is this possible when all systems today claim to have automatic escaping? Automatic escaping without knowledge of context is a bit of a fallacy, which creates a false sense of security.
Templating systems like Twig, Laravel Blade, and others do not see any HTML structure in the template. Therefore, they do not see contexts either. Compared to Latte, they are blind and naive. They only process their own tags; everything else is an insignificant stream of characters to them:
░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░
░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░
- in text: <span>{{ foo }}</span>
- in tag: <span {{ foo }} ></span>
- in attribute: <span title='{{ foo }}'></span>
- in unquoted attribute: <span title={{ foo }}></span>
- in attribute containing URL: <a href="{{ foo }}"></a>
- in attribute containing JavaScript: <img onload="{{ foo }}">
- in attribute containing CSS: <span style="{{ foo }}"></span>
- in JavaScript: <script>var = {{ foo }}</script>
- in CSS: <style>body { content: {{ foo }}; }</style>
- in comment: <!-- {{ foo }} -->
Naive systems just mechanically convert the characters < > & ' "
to HTML entities, which, while a valid
method of escaping in most use cases, is far from always sufficient. Thus, they cannot detect or prevent the creation of various
security holes, as we will show below.
Latte sees the template just like you do. It understands HTML, XML, recognizes tags, attributes, etc. And thanks to this, it distinguishes individual contexts and treats data accordingly. It thus offers truly effective protection against the critical Cross-site Scripting vulnerability.
░░░░░░░░░░░<span>{$foo}</span>
░░░░░░░░░░<span {$foo} ></span>
░░░░░░░░░░░░░░░░<span title='{$foo}'></span>
░░░░░░░░░░░░░░░░░░░░░░░░░<span title={$foo}></span>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<a href="{$foo}"></a>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<img onload="{$foo}">
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<span style="{$foo}"></span>
░░░░░░░░░░░░░░░░░░<script>░░░░░░{$foo}</script>
░░░░░░░░░░<style>░░░░░░░░░░░░░░░░{$foo}░░░</style>
░░░░░░░░░░░░░░<!--░{$foo}░-->
- in text: <span>{$foo}</span>
- in tag: <span {$foo} ></span>
- in attribute: <span title='{$foo}'></span>
- in unquoted attribute: <span title={$foo}></span>
- in attribute containing URL: <a href="{$foo}"></a>
- in attribute containing JavaScript: <img onload="{$foo}">
- in attribute containing CSS: <span style="{$foo}"></span>
- in JavaScript: <script>var = {$foo}</script>
- in CSS: <style>body { content: {$foo}; }</style>
- in comment: <!-- {$foo} -->
Live Demonstration
On the left, you see the template in Latte; on the right is the generated HTML code. The variable $text
is printed
several times, each time in a slightly different context. And thus, escaped slightly differently. You can edit the template code
yourself, for example, change the content of the variable, etc. Try it:
Isn't that great! Latte performs context-aware escaping automatically, so the programmer:
- doesn't need to think about or know how to escape where
- cannot make a mistake
- cannot forget about escaping
These are not even all the contexts that Latte distinguishes when printing and for which it adapts data handling. We will now go through other interesting cases.
How to Hack Naive Systems
Using several practical examples, we will 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 an HTML attribute, as we showed above. Let's have a template in Twig rendering an image:
<img src={{ imageFile }} alt={{ imageAlt }}>
Notice that there are no quotes around the attribute values. The coder might have forgotten them, which simply happens. For example, in React, code is written this way, without quotes, and a coder who switches between languages can easily forget the quotes.
An attacker inserts a cleverly crafted string foo onload=alert('Hacked!')
as the image caption. We already know
that Twig cannot determine whether a variable is being printed in the HTML text flow, inside an attribute, an HTML comment, etc.;
in short, it does not distinguish contexts. And it just mechanically converts the characters < > & ' "
into
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 forged 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 the variable is being printed as the value of an attribute that is not enclosed in quotes. Therefore, 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-aware escaping, it is possible to use PHP variables natively within JavaScript.
<p onclick="alert({$movie})">{$movie}</p>
<script>var movie = {$movie};</script>
If the variable $movie
contains the string 'Amarcord & 8 1/2'
, the following output will be
generated. Notice the different escaping used within HTML compared to within JavaScript, and yet another different escaping in the
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 a variable used in src
or href
attributes contains a web URL
(i.e., HTTP protocol) and prevents the output of links that could pose a security risk.
{var $link = 'javascript:attack()'}
<a href={$link}>click here</a>
Outputs:
<a href="">click here</a>
The check can be disabled using the nocheck filter.
Limits of Latte
Latte is not a complete XSS protection for the entire application. We would be unhappy if you stopped thinking about security when using Latte. Latte's goal is to ensure that an attacker cannot alter the page structure, forge HTML elements or attributes. But it does not check the content correctness of the printed data. Nor the correctness of JavaScript behavior. That goes beyond the competence of the templating system. Verifying the correctness of data, especially data entered by the user and therefore untrusted, is an important task for the programmer.