Click here to share this article on LinkedIn »

Beefing up Security

If I told you about an easy way to make your site nearly invincible to cross-site scripting (XSS) attacks, would you use it?

Well, a Content Security Policy (CSP) can do that! It’s as simple as adding an extra header when you serve an html file from the server.

I’ll link to more information about how you can set this up for your site at the bottom of this post. For now, here’s an example of a strong CSP.

Content-Security-Policy: default-src 'self'; form-action 'self';

Based on this example header browsers prevent any inline scripts or styles from running and will only allow resources such as scripts, styles, fonts, etc. to load from our own domain.

And therein lies the catch: no inline JavaScript

Use case

Allow me to set the stage. We’re building a web app, and we need some data from the server. We’re migrating an existing service to the wonders of the New World. Progressive Web App and all that jazz. However this data we need is terribly inconvenient to get, because it’s not available through any web API.

Our server uses templates to show dynamic content to the user, such as their name, their age, and what their dog had for breakfast! 🐶

In order for a page to use data from the server in a client side script, you may have seen a pattern like this before:

<!-- user-template.html - On the server -->

<script type="text/javascript">

var firstName = "{{ user.firstName }}";

var age = {{ user.age }};

// the 'dump' filter calls JSON.stringify on the object

var pet = {{ user.pet | dump }}; document.addEventListener("DOMContentLoaded", function() {

//do work with the variables once the page is ready...

});

</script>

But now, if we serve the above code in our web application with a CSP header like above, we’ll see an error message in our browser console that this script was not executed.

Example of a script blocked by a CSP

Note: Most times this data can (and probably should) come from API calls, however there are cases we will run into where that’s not an option.

A Solution: Inline JSON

Luckily there’s another easy, and secure, solution.

Instead of inline JavaScript, we can create a <script> tag that has type application/json . The browser will not evaluate this as JavaScript so it will pass the CSP.

<!-- user-template.html -->

<script type="application/json" data-my-app-selector="user-data">

{

"firstName": "{{ user.firstName }}",

"age": {{ user.age }},

"pet": {{ user.pet | dump }}

}

</script>

<script src="main.js"></script>

Then in our main JavaScript file we’ll parse the JSON and take the value to use in our app.

// main.js

function getData(dataSelect) {

try {

const inlineJsonElement = document.querySelector(

`script[type="application/json"][data-my-app-selector="${dataSelect}"]`

);

const data = JSON.parse(inlineJsonElement.textContent);

return data;

} catch (err) {

console.error(`Couldn't read JSON data from ${dataSelect}`, err);

}

} document.addEventListener("DOMContentLoaded", function() {

// do work with the variables once the page is ready

const user = getData("user-data");

console.log(user.name);

// ...

});

This pattern allows us to have a secure CSP, and still inline data from the server! Mission accomplished!

All code in this post is available in a GitHub repo. You can check it out and play with the various secure / insecure options.