How Cross-Site Scripting Attacks Hack Your Website
If you're a newbie when it comes to web development and security, it can be difficult to imagine exactly how a website gets hacked. When we create a website, it only runs the code we write - so how can someone use it to run code on someone else's machine?
Let's not kid ourselves, security is important. Users are our lifeblood, and as a web developer it's our responsibility to protect their data, privacy, and the trust we've worked so hard to build.
It's hard to discuss every possible attack in a single blog post - there is an entire field of research dedicated to it. Instead, I'm going to take you through how vulnerabilities work, and show you the most common type of attack - a Cross-site Scripting attack (XSS).
What is a security vulnerability?
At a high level, security vulnerabilities are a weakness in a program or system that can be exploited by attackers.
An exploit generally have one of two short-term outcomes:
- Get information from a system that the hacker wasn't supposed to get
- Cause the system to behave in a way that was not intended. (the golden goose is the hacker gaining administrator privileges)
The goal of an attack can vary wildly, depending on what your website. A common goal is to steal sensitive information, gain control of your system, or generally cause chaos.
Cross-site Scripting (XSS) and how it works
The short explanation of an XSS attack is when an attacker injects their JavaScript into your site - and it can be surprisingly easy to do.
This class of attack is called arbitrary code execution. This means that attackers are able to run any code they want on your site – and that gives them a lot of power.
Through XSS, attackers are able to:
- Read and write the content of your page
- Redirect people to a different site
- Read any data stored in your site's Cookies or LocalStorage
- Intercept key and mouse events
- Send requests to your APIs using your user's credentials
The core of an XSS attack is about getting un-escaped <script>
tags, or JavaScript-executing parameters like onclick
or onerror
into your HTML.
Say I have a profile page at example.com/profile
that displays my name to other users. It might have markup like this:
<body>
<h1>
Carl Anderson
</h1>
<section class="about-me">
<!-- blah blah blah -->
</section>
</body>
Since this is my profile, I'm allowed to change my name - so I change my name to <script>alert("Hello")</script>
(or <img src="" onerror="alert('Hello')">
)
This means that the HTML when for my profile now looks like this:
<body>
<h1>
<script>alert("Hello")</script>
</h1>
<section class="about-me">
<!-- blah blah blah -->
</section>
</body>
This is completely valid HTML, and since <script>
tags will run anywhere on the page, anyone who visits my page will have a site popup that says "Hello".
But we're using <script>
tags! It can do more than inject inline JavaScript.
Suppose I change my name to <script src="/malicious/resource/here.js" />
. Now I can run any JavaScript I want. I can do anything your site can do... like change their name to inject my malicious script.
The potential damage caused by one of these attacks depends largely on what the vulnerable website is for. If it simply stores home recipes, it's probably nothing more than an annoyance; but if it stores your sensitive financial data, it can constitute a major breach.
Preventing an XSS attack
Unfortunately, there are numerous ways an XSS attack can form, so stopping them requires careful analysis.
The golden rule is that all user input is a threat. You should not insert it anywhere on a page without defending against XSS attacks.
Method 1: Use a Framework
By far the best way to defend against XSS attacks is to use a framework like React or Angular. These frameworks are written with XSS in mind, and will automatically defend against them.
That's not to say these are silver bullets – there is still an XSS risk in frameworks. What frameworks do well is make XSS threats obvious. For instance, React's XSS vulnerabilities tend to come from using its dangerouslySetInnerHTML
parameter.
This by-default protection makes it much less likely for you as a developer to slip up and let one of these vulnerabilities through.
Method 2: HTML Entity Encoding
If you can't rewrite your entire front-end to use a framework or you need to make use of dangerouslySetInnerHTML
, the way to prevent XSS attacks is through HTML entity encoding.
This works by preventing users from injecting characters that could be used for malicious code. The encoded <
still renders as <, but won't be interpreted as HTML tags.
In JavaScript, you can get this encoding by using the textContent
field to inject content, which automatically escapes text before inserting it into the DOM.
If you're using any other approach, like setting innerHTML
or appendChild
, you're going to need to make sure any user input is encoded yourself. This is also true if you're rendering your site on the server before passing it to the client.
let unsafeText = responseText;
let div = document.createElement('div');
// textContent automatically escapes the unsafe text
// innerHTML would insert it as HTML, and is vulnerable to XSS
div.textContent = unsafeText;
element.appendChild(div):
Some languages, like PHP provide a default way of performing entity encoding through functions like htmlentities()
. There is no JavaScript equivalent but if you want to use similar functionality, we can create our own:
function htmlEntities(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
div.innerHTML = `<div class="someClass">${htmlEntities(user.name)}</div>`