Developers are not Trusted Types!
Developers are not Trusted Types!
By Dave Bitter
8 min read
Even with our best intentions as developers, we all make mistakes. XSS being one of the most common web vulnerabilities on the web proves that we need to better defend ourselves and our users against this. Let’s see how Trusted Types can help us!
- Authors
- Name
- Dave Bitter
- linkedinDave Bitter
- twitter@dave_bitter
- Github
- githubDaveBitter
- Website
- websiteBlog
At iO, we continuously work on improving our skills. Recently quite a few developers, including myself, followed a two-day training course on web security by Philippe De Ryck. This course covered a wide arrange of topics, but one technique, in particular, stood out to me. He showed how you can use Trusted Types in your Content Security Policy (CSP) to protect yourself against cross-site scripting (XSS) attacks. Let’s have a look at what Trusted Types are, why you want to use this technique and how you can use it.
What is XSS?
Firstly, let’s have a quick look at what XSS is again and how you could easily become vulnerable to this attack. XSS is a vulnerability that allows an attacker to inject malicious code into your website. Let’s look at a basic example of how this works in, for instance, a Single Page Application (SPA) if you don’t protect yourself. Let’s say you are building a comment section for this website (please do). This website is built using React.js so we’ll use that for the code examples. You might have a component that displays some data that is fetched from an API:
import React from 'react'
const Comments = ({ comments }) => {
return (
<ul>
{comments.map(({ id, content, authorName }) => (
<li key={id}>
<strong>{authorName} wrote: </strong>
<span dangerouslySetInnerHTML={{ __html: content }} />
</li>
))}
</ul>
)
}
export default Comments
If you have worked with React.js before, you probably used dangerouslySetInnerHTML
before. So why do developers use this and why is it dangerous? dangerouslySetInnerHTML
is used when you need to inject some HTML instead of a string. In this example, the comment was written in a what-you-see-is-what-you-get-editor (WYSIWYG). The editor then creates the HTML of the content and sends that over during the submission of the comment. This HTML “string” is then stored in the database. You as a developer then fetch all the comments and want to render that bit of HTML in the DOM. You don’t want to render the HTML as a string, hence the quotation marks, but want to create the DOM elements out of it. For this dangerouslySetInnerHTML
is used.
innerHTML
?
What is so dangerous about setting Well, not much, if you know what is being set(!). Even then, you are putting a lot of faith in it not being malicious. If you inject HTML that a user has written through a WYSIWYG-editor, you can’t trust it. Ever!
For instance, a malicious user can add this comment on your article page:
Hey,
Malicious comment here:
<img src='none' alt='purposely erroring image attack' onerror='window.alert("hello!")'/>
Note that most attackers won’t be so nice to let you know the content is malicious. Let’s dissect this attack. The attacker adds an image element that has a src
of “none”. This src
will not resolve. Because of that, an error will be thrown. The attacker therefore adds an onerror
attribute where they can execute JavaScript. In this case, there will be a harmless alert shown, but you can imagine that they can now load whatever JavaScript they want.
How can I protect myself against this attack?
You can protect yourself from this attack by firstly never trusting any content that you receive to be directly injected into the DOM. You always want to make sure to handle this potentially malicious content accordingly.
By using innerHTML
, you’re essentially telling the browser to inject en parse any HTML that you pass. Regardless of what it does. This way, the onerror
will be added to the image element and you are vulnerable to the attack.
There are a couple of ways of doing this more safely.
Using safe DOM APIs
You can make use safe DOM APIs to inject and parse the content. For instance, you could create a paragraph element where you set the textContent
:
const commentParagraph = document.createElement('p')
commentParagraph.textContent = comment.content
document.getElementById('comments').appendChild(commentParagraph)
By using the textContent
API, you tell the browser that you have some text content that you want to show. If this content contains malicious code, this will just be added as text content, not as actual code in the DOM.
Using sanitization on the output
Now, earlier I said that the comment contains HTML that the WYSIWYG editor generated. This HTML code is in fact what we want. It might contain some perfectly fine elements like ol
, ul
, li
, strong
and so on. So how can you keep the safe HTML, but strip out the parts like the onerror
attribute on the image?
You can make use of a package like DOMPurify. This package sanitises the content it is passed with sensible defaults which you can always tweak if you know what you are doing. Let’s go back to the React.js Example and add DOMPurify:
import React from 'react'
import * as DOMPurify from 'dompurify'
const Comments = ({ comments }) => {
return (
<ul>
{comments.map(({ id, content, authorName }) => (
<li key={id}>
<strong>{authorName} wrote: </strong>
<span dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }} />
</li>
))}
</ul>
)
}
export default Comments
Now when the content is injected and parsed in the DOM, the onerror
attribute will be stripped. Perfect, problem solved! Or not?
Don’t trust yourself and your fellow developers
Yes, DOMPurify will sanitise the content and prevent an XSS attack from being executed. There is one big if, however. If you forget this sanitization once, you are vulnerable again. Now, you could add linting rules, code review processes and other attempts to prevent this, but inevitably someone will get passed this (e.g. turn off the linting rule) and you are vulnerable again. Aren’t there better ways?
How can I use CSP to protect myself?
You can use CSP for quite a few things, but let’s focus on the example at hand. If malicious JavaScript being executed is the real issue, can’t you just block that? You can! I’ve built a demo website to showcase different solutions. Firstly, let’s see what happens when you don’t have any CSP on your page showcased here.
Once you hit the injection button, the following code is executed and the XSS vulnerability is exploited:
const elements = {
injectionForm: document.querySelector('[data-injection-form]'),
injectionFormInput: document.querySelector('[data-injection-form-input]'),
injectionFormOutput: document.querySelector('[data-injection-form-output]'),
}
const handleSubmit = (e) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const input = formData.get('input')
elements.injectionFormOutput.innerHTML = input
}
elements.injectionForm.addEventListener('submit', handleSubmit)
Adding a basic CSP header
You can add a basic CSP header that looks like this:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'" />
Now when you hit the injection button the malicious JavaScript of the onerror
attribute will not be executed as showcased here:
You can see the error thrown by the CSP in the console. To clean this up a bit up you can use DOMPurify to remove the onerror
attribute just like we did before so you will just see this message if you forgot to sanitise somewhere:
Perfect, all done! Right?
Adding a Trusted Types CSP header
No, you could even take it a step further by adding the Trusted Types CSP header. Beware, this is an experimental feature that is driven by Google. The current support is:
First, you add the Trusted Types CSP header:
<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'" />
Now when you hit the injection button none of the content is injected and parsed in the DOM:
Next to that, it shows a more detailed error message for you as a developer as to where the innerHTML
was used to try to inject and parse the content in the DOM.
Next, you can configure a Trusted Types Policy:
if (window.trustedTypes && trustedTypes.createPolicy) {
trustedTypes.createPolicy('default', {
createHTML: (string) => string.replace(/\</g, '<').replace(/\>/g, '>'),
})
}
Note that you have to feature detect whether Trusted Types a supported. This does seem like some manual work where you can easily make some mistakes as defending against XSS can get quite complex. Can’t you just use DOMPurify for this and ask it to both sanitise and give back a Trusted Type. That way you can rely on that package and get the sanitised content to inject and parse in the DOM.
Let’s revisit the earlier example using DOMPurify and ask for a Trusted Type as well:
import React from 'react'
import * as DOMPurify from 'dompurify'
const Comments = ({ comments }) => {
return (
<ul>
{comments.map(({ id, content, authorName }) => (
<li key={id}>
<strong>{authorName} wrote: </strong>
<span
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(content, { RETURN_TRUSTED_TYPE: true }),
}}
/>
</li>
))}
</ul>
)
}
export default Comments
Now when you hit the injection button the malicious JavaScript of the onerror
attribute will not be executed and the safe content will be injected and parsed as showcased here:
Develop with the Trusted Types CSP header set
One of the great benefits I see is that this is a perfect tool during development as well. Make sure to set the CSP header for Trusted Types during development. This way, even if your content does not have potentially risky attributes like onerror
which are being injected and parsed in the DOM, you will notice when you don’t use the correct sanitization with a Trusted Type. You will simply not see your content on the screen and get a helpful error message.
Even with our best intentions as developers, we all make mistakes. XSS being one of the most common web vulnerabilities on the web proves that we need to better defend ourselves and our users against this. I’m curious to see where Trusted Types are headed and hope I inspired you to try it out!
For a more in-depth view into Trusted Types, head over to the article by Phillip De Ryck. To have a look at the demo code for this article, head over to the repository on GitHub.
Thanks for reading and stay safe!
Upcoming events
Coven of Wisdom - Herentals - Winter `24 edition
Worstelen jij en je team met het bouwen van schaalbare digitale ecosystemen of zit je vast in een props hell met React of in een ander framework? Kom naar onze meetup waar ervaren sprekers hun inzichten en ervaringen delen over het bouwen van robuuste en flexibele applicaties. Schrijf je in voor een avond vol kennis, heerlijk eten en een mix van creativiteit en technologie! 🚀 18:00 – 🚪 Deuren open 18:15 – 🍕 Food & drinks 19:00 – 📢 Building a Mature Digital Ecosystem - Maarten Heip 20:00 – 🍹 Kleine pauze 20:15 – 📢 Compound Components: A Better Way to Build React Components - Sead Memic 21:00 – 🙋♀️ Drinks 22:00 – 🍻 Tot de volgende keer? Tijdens deze meetup gaan we dieper in op het bouwen van digitale ecosystemen en het creëren van herbruikbare React componenten. Maarten deelt zijn expertise over het ontwikkelen van een volwassen digitale infrastructuur, terwijl Sead je laat zien hoe je 'From Props Hell to Component Heaven' kunt gaan door het gebruik van Compound Components. Ze delen praktische inzichten die je direct kunt toepassen in je eigen projecten. 📍 Waar? Je vindt ons bij iO Herentals - Zavelheide 15, Herentals. Volg bij aankomst de borden 'meetup' vanaf de receptie. 🎫 Schrijf je in! De plaatsen zijn beperkt, dus RSVP is noodzakelijk. Dit helpt ons ook om de juiste hoeveelheid eten en drinken te voorzien - we willen natuurlijk niet dat iemand met een lege maag naar huis gaat! 😋 Over iO Wij zijn iO: een groeiend team van experts die end-to-end-diensten aanbieden voor communicatie en digitale transformatie. We denken groot en werken lokaal. Aan strategie, creatie, content, marketing en technologie. In nauwe samenwerking met onze klanten om hun merken te versterken, hun digitale systemen te verbeteren en hun toekomstbestendige groei veilig te stellen. We helpen klanten niet alleen hun zakelijke doelen te bereiken. Samen verkennen en benutten we de eindeloze mogelijkheden die markten in constante verandering bieden. De springplank voor die visie is talent. Onze campus is onze broedplaats voor innovatie, die een omgeving creëert die talent de ruimte en stimulans geeft die het nodig heeft om te ontkiemen, te ontwikkelen en te floreren. Want werken aan de infinite opportunities van morgen, dat doen we vandaag.
| Coven of Wisdom Herentals
Go to page for Coven of Wisdom - Herentals - Winter `24 editionThe Test Automation Meetup
PLEASE RSVP SO THAT WE KNOW HOW MUCH FOOD WE WILL NEED Test automation is a cornerstone of effective software development. It's about creating robust, predictable test suites that enhance quality and reliability. By diving into automation, you're architecting systems that ensure consistency and catch issues early. This expertise not only improves the development process but also broadens your skillset, making you a more versatile team member. Whether you're a developer looking to enhance your testing skills or a QA professional aiming to dive deeper into automation, RSVP for an evening of learning, delicious food, and the fusion of coding and quality assurance! 🚀🚀 18:00 – 🚪 Doors open to the public 18:15 – 🍕 Let’s eat 19:00 – 📢 First round of Talks 19:45 – 🍹 Small break 20:00 – 📢 Second round of Talks 20:45 – 🍻 Drinks 21:00 – 🙋♀️ See you next time? First Round of Talks: The Power of Cross-browser Component Testing - Clarke Verdel, SR. Front-end Developer at iO How can you use Component Testing to ensure consistency cross-browser? Overcoming challenges in Visual Regression Testing - Sander van Surksum, Pagespeed | Web Performance Consultant and Sannie Kwakman, Freelance Full-stack Developer How can you overcome the challenges when setting up Visual Regression Testing? Second Round of Talks: Omg who wrote this **** code!? - Erwin Heitzman, SR. Test Automation Engineer at Rabobank How can tests help you and your team? Beyond the Unit Test - Christian Würthner, SR. Android Developer at iO How can you do advanced automated testing for, for instance, biometrics? RSVP now to secure your spot, and let's explore the fascinating world of test automation together!
| Coven of Wisdom - Amsterdam
Go to page for The Test Automation MeetupMastering Event-Driven Design
PLEASE RSVP SO THAT WE KNOW HOW MUCH FOOD WE WILL NEED Are you and your team struggling with event-driven microservices? Join us for a meetup with Mehmet Akif Tütüncü, a senior software engineer, who has given multiple great talks so far and Allard Buijze founder of CTO and founder of AxonIQ, who built the fundaments of the Axon Framework. RSVP for an evening of learning, delicious food, and the fusion of creativity and tech! 🚀 18:00 – 🚪 Doors open to the public 18:15 – 🍕 Let’s eat 19:00 – 📢 Getting Your Axe On Event Sourcing with Axon Framework 20:00 – 🍹 Small break 20:15 – 📢 Event-Driven Microservices - Beyond the Fairy Tale 21:00 – 🙋♀️ drinks 22:00 – 🍻 See you next time? Details: Getting Your Axe On - Event Sourcing with Axon Framework In this presentation, we will explore the basics of event-driven architecture using Axon Framework. We'll start by explaining key concepts such as Event Sourcing and Command Query Responsibility Segregation (CQRS), and how they can improve the scalability and maintainability of modern applications. You will learn what Axon Framework is, how it simplifies implementing these patterns, and see hands-on examples of setting up a project with Axon Framework and Spring Boot. Whether you are new to these concepts or looking to understand them more, this session will provide practical insights and tools to help you build resilient and efficient applications. Event-Driven Microservices - Beyond the Fairy Tale Our applications need to be faster, better, bigger, smarter, and more enjoyable to meet our demanding end-users needs. In recent years, the way we build, run, and operate our software has changed significantly. We use scalable platforms to deploy and manage our applications. Instead of big monolithic deployment applications, we now deploy small, functionally consistent components as microservices. Problem. Solved. Right? Unfortunately, for most of us, microservices, and especially their event-driven variants, do not deliver on the beautiful, fairy-tale-like promises that surround them.In this session, Allard will share a different take on microservices. We will see that not much has changed in how we build software, which is why so many “microservices projects” fail nowadays. What lessons can we learn from concepts like DDD, CQRS, and Event Sourcing to help manage the complexity of our systems? He will also show how message-driven communication allows us to focus on finding the boundaries of functionally cohesive components, which we can evolve into microservices should the need arise.
| Coven of Wisdom - Utrecht
Go to page for Mastering Event-Driven Design