Have you been considering your app's security this month? This Halloween, here's some security advice to help keep you safe from hackers and avoid any spooky surprises.
October is National Cyber Security Awareness Month, so it’s a great excuse to talk about one of the most common types of attacks to web applications, Cross-Site Scripting, and how to mitigate it using a security feature present in all modern browsers called Content Security Policy.
Cross-Site Scripting, or XSS for short, is one of the most common security issues in web applications these days. In XSS, an attacker manages to inject their own code into our app. This code will then be executed when users visit the page, so it can do anything from stealing their cookies to logging all their key presses.
To demo this, we’ve built a small app with a form that is vulnerable to XSS. The app works fine: we enter some text in the
textarea element, press the Submit button, and our comment gets appended to the list. However, notice what happens when we enter HTML in the field, like
React is <b>awesome</b>!:
That’s a cool feature, right? The user can style their comments however they want!
Well, let’s try entering another bit of HTML,
<img src="nope.jpg" onerror="alert('Hacked!')" />:
Oops, we’ve been hacked! How did that happen? If we look at the HTML we entered, we’ll see that the
<img> tag pointed to a non-existent image, so the
(Try playing with the form yourself, see if you can find other ways to execute code through that form. If you want spoilers, look at the OWASP XSS cheat sheet.)
React tries to protect us from XSS attacks by escaping all strings we render as children of an element. If we really want to render unescaped HTML, we need to use a special prop appropriately named
While there are valid use cases for
dangerouslySetInnerHTML, such as rendering sanitized HTML coming from your server, I’d try to minimize its use, because it’s really easy to do something wrong and open ourselves up for an attack.
The problem underlying an XSS attack is that the browser doesn’t know which sources of code to trust. My
<script src="https://cdn.jsdelivr.net/npm/react@latest/umd/react.production.min.js"></script> tag and your injected
<img src="nope.jpg" onerror="alert('Hacked!')" /> tag are both valid, so the browser will execute them all.
Content Security Policy is a security feature present in all modern browsers that allows us to list which sources are trusted by our web application, so that the browser is able to block everything else. If your name is on the guest list, you can get into the party; otherwise, you’re staying outside.
There are two ways to declare a Content Security Policy: through a
Content-Security-Policy HTTP header when serving your HTML page, and through a
<meta http-equiv="Content-Security-Policy"> tag.
In our sample app, we don’t have control over HTTP headers, so we’ll use a
<meta> tag. Let’s add the most restrictive Content Security Policy possible,
default-src 'none', which basically tells the browser to not trust any source, and see what happens:
Well, it looks like everything broke:
The error messages are somewhat cryptic, but we can figure them out by looking things up in the Content Security Policy article in MDN, which describes all possible directives and values we can specify.
Let’s go one by one.
The first error has to do with scripts getting blocked:
We didn’t provide a
script-src policy, so the browser fell back to what’s specified as
default-src (which is
'none') and refused to load all scripts. It looks like the folks hosting our sample app, StackBlitz, are running our code through
eval, so we’ll need to allow that as a source in our
(Note that you wouldn’t want to do this in a real app. Allowing any of the
unsafe-* sources like
unsafe-inline is a probably a really bad idea!)
The second error has to do with styles getting blocked:
Refused to load the stylesheet ‘https://cdn.jsdelivr.net/npm/@progress/kendo-theme-default@latest/dist/all.css’ because it violates the following Content Security Policy directive: “default-src ‘none’”. Note that ‘style-src’ was not explicitly set, so ‘default-src’ is used as a fallback.
Same thing, we didn’t provide a
style-src policy, so the browser fell back to
default-src and refused to load all styles. We can specify the exact path to the stylesheet, a partial path, or just the domain. We’ll specify a partial path here, so that all styles coming from
@progress-scoped packages are trusted:
The final errors have to do with fonts getting blocked:
Refused to load the font ‘data:font/ttf;base64,…’ because it violates the following Content Security Policy directive: “default-src ‘none’”. Note that ‘font-src’ was not explicitly set, so ‘default-src’ is used as a fallback.
The CSS file above seems to be inlining base64-encoded fonts using the
data: protocol, so we’ll need to add a
font-src directive that allows for that:
Now the app is working again! Let’s try our original attack by injecting
<img src="nope.jpg" onerror="alert('Hacked!')" /> into the page:
CSP blocked the attack! Here’s the error message we got:
Refused to execute inline event handler because it violates the following Content Security Policy directive: “script-src ‘unsafe-eval’”. Either the ‘unsafe-inline’ keyword, a hash (‘sha256-…’), or a nonce (‘nonce-…’) is required to enable inline execution.
We didn’t allow inline scripts as a trusted source, so the browser prevented the code from running. 🎉🎉🎉
Ok, this is really cool, but there’s no way I’m going to add a Content Security Policy to my app! What if I misconfigure it and I break some important feature for my users?
Well, the people behind the CSP spec must have thought of that exact scenario. They’ve added a report-only mode that will output errors to the console, but not block execution. So if we were to add this tag to our app:
we would see all the CSP violations happening in the app, but it would keep working just fine.
We can go one step further, and use the
report-uri directive to tell the browser to send all violations to an endpoint under our control, so that we can aggregate them in a dashboard or something:
Whenever the browser encounters a violation of our CSP, it will make a
POST request to
/api/csp with content that looks like this:
Content Security Policy is a really powerful tool that modern browsers provide to prevent Cross-Site Scripting attacks. My advice is to start with the most restrictive policy possible,
default-src 'none', and go from there. Use
Content-Security-Policy-Report-Only to aggregate reports and ensure your CSP is configured correctly, so that you don’t break any functionality for your users. Once you’re sure everything is configured correctly, switch to
Content-Security-Policy to enforce the policy for real, and stop the bad guys from doing their evil.
Subscribe to be the first to get our expert-written articles and tutorials for developers!