Three Steps For Increasing The Security of Your Web Apps
Jamie
Posted on September 13, 2018
I recently shared the following QR code with my work's Slack group:
Pro tip: never just scan a random QR code without checking it with a service like this first
This QR code isn't malicious, it simply links to the site for my new podcast (https://dotnetcore.show/
), and I'm happy to say that everyone at work passed. That is, I saw no new requests for the site, from IP addresses near where I work.
I think this proves a point that I'm about to make in the post. And that point is: us developers are the experts at development, and our opinions on development matters.
A Real-World Example
The story which prompted this post was one about Feedify, and all of their customers, being hacked. Here's a link to the first place I saw the story: https://www.bleepingcomputer.com/news/security/feedify-hacked-with-magecart-information-stealing-script/
Reading this blog post lead me on an opinionated twitter rant about security in webapps
Unfortunately for Feedify I picked on them during this rant, but they are far from the first to be hit with this kind of attack. Earlier this year, many of the websites for different branches of the UK Government had a Crypto Miner injected into them:
In both of these examples, a third party script was compromised and JS for a further third party (a sixth party?) was injected into it. Or written another way:
- my amazing e-commerce site includes a script from Feedify
- Feedifys script is compromised and already has some other script embedded into it
- my amazing e-commerce site now includes the compromised script.
But how can you stop this from happening? It turns out that its stupidly simple, and only takes a maximum of three steps.
Stop Using unsafe-eval
The way that Feedifys script was originally included was along the lines of:
var s = document.createElement('script');
s.type = 'text/javascript';
s.src = 'https://cdn.feedify.net/getjs/feedbackembed-min-1-0.min.js';
document.getElementByTagName('head')[0].appendChild(s);
This should immediately be ringing alarm bells. Why are we adding a script via JS? Couldn't we do exactly the same by adding
<script type='text/javascript'
src='https://cdn.feedify.net/getjs/feedbackembed-min-1-0.min.js'>
</script>
Which is the accepted practise. In fact, the first code snippet does exactly this, but at runtime rather than design time.
What's Wrong With That First Snippet?
Aside from being slow (the page has to be downloaded and all JS needed to be parsed before adding the news script to the head
element), it's incredibly insecure.
By injecting scripts into the page like this, there's no way to check what has actually been added to the page until after the page has been rendered. This also means that you are allowing the JS engine in the browser to load and evaluate a script from an external source without you ever having checked it.
The same can also be said about scripts loaded using the second code snippet. But there are two big differences:
- the second snippet is how you're meant to do it
- you can perform a check, at runtime, that the script was loaded correctly using SRI
Subresrource Integrity basically tells the browser to do a has check on the script that it downloaded. This requires the script author giving you a hash value for their script, but you can easily generate one at the command line or using a service like this one
If the hash of the delivered JS file fails the check against the supplied hash, the browser raises an error in the console and refuses to parse the delivered script.
Because the original script delivered by Feedify was altered, it's hash value wouldn't have passed the SRI check.
Adding an SRI check for a JS file is as simple as the following (the hash value will be different for each script, obviously):
<script type='text/javascript'
src='https://cdn.feedify.net/getjs/feedbackembed-min-1-0.min.js'
integrity='sha256-3edrmyuQ0w65f8gfBsqowzjJe2iM6n0nKciPUp8y+7E='>
</script>
I've used the SRI for jquery 3.1 here, because the Feedify script is gone from the web
CSP
Content Security Policy is an HTTP header which instructs the browser where it is allowed to load resources from. As it's an HTTP header, it's delivered shortly before the HTML for the page and sets up the rules for the current session.
If the domain isn't listed in the CSP, all requests to that domain will raise errors and will be blocked.
Setting up a CSP takes a little more effort. This is because it comes from the server, rather than the HTML itself. But all webservers have the ability to add custom headers, so it's not like its difficult to do.
The difficult part is getting it right.
Because modern webapps load resources from all over the web, you need to white list every single domain that your app loads from. And since the rules apply to all resource types (scripts, images, frames, CSS, etc.) each resource type has its own rule. This means that your CSP rule for scripts should look different to the CSP rule for CSS.
As an example, here is a redacted version of the CSP rule for https://dotnetcore.show/:
upgrade-insecure-requests;
default-src 'self';
connect-src 'self' https://cdn.jsdelivr.net https://api.unsplash.com;
script-src 'self' https://cdnjs.cloudflare.com https://code.jquery.com;
This reads (to the browser) like:
- Every request for something over http should be auto upgraded to https
- Unless stated in other rules, only allow resources from:
- Only allow scripts to be loaded from:
Anything else will be blocked by the browser, before the request is generated. This also covers resources requested from within those scripts.
The compromised Feedify script contained a request used by an injected MageCart script. The request was sent to info-stat[.]ws
(I've added square brackets around the '.' char so that your browser doesn't make it into a clickable link)
A CSP which whitelisted the Feedify CDN domain but NOT the info-stat domain would have caused the requests to that domain fail, and for errors to be logged in the console. These errors could have been picked up in Dev, QA, and UAT environments, so would have been fixed by developers working on the affected webapps.
However, this would have required the use of the standard way of including scripts (i.e the removal of the unsafe eval pointed out above).
And Finally
This is the opinionated and slightly controversial point of the post.
4000 sites were hit with this attack.
That means 4000 dev teams (assuming that each site was created by an individual team) either didn't know about the security issues with using unsafe eval or that CSP could be used to secure their websites.
If this is the case, that means a huge number of dev teams either didn't know about these security measures
...
OR they did know, and didn't push back hard enough when decision makers decided not to invest time in security.
We need to remember that we are the experts in the development space. We're hired specifically because we are experts at this. As such, it's our duty to know about these things and push back as hard (if not harder) when the powers that be decide not to invest in security.
When a breech or security issue happens, it'll be our butts on the line not those of the decision makers. Simply raising these points, having them logged, and moving on quietly is not the way to do it.
Put yourself in the place of the users who were caught in the breech, think of what it's like to have to have to deal with having your identity stolen - worse yet, your life savings stolen - because some developer didn't want to feel slightly uncomfortable in bringing up the possibility of a security breech.
Would you want to be that person? I know that I wouldn't.
Posted on September 13, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.