Cross-Site Scripting (XSS) vulnerabilities are quite common and serious. However, they can largely be avoided by following secure development practices, and best of all, the web application can be hardened with CSP (Content Security Policy), which can make XSS attacks nearly impossible.
1. Protect the application with CSP
Content Security Policy (CSP) is a browser security control that websites can voluntarily adopt to protect against XSS and other client-side threats by sending the Content-Security-Policy header in their HTTP responses.
The basic principle of CSP is to increase the security of a website by limiting what can happen on the site and from where resources such as scripts can be loaded.
CSP is the browser-side implementation of the principle of least privilege, which means that the application is given only the necessary privileges. This way, in the event of an attack, the attacker has limited abilities to cause damage.
You can read more about CSP and practice its use here.
2. If you are building dynamic HTML, use a dedicated template library for it
If you have a traditional-style application where HTML is constructed on the fly and sent to the browser, use a template library suitable for building HTML such as Jinja2 and ensure that the library can automatically encode the parameters correctly.
Also, do not use insecure functions from template libraries if they exist, such as the Jinja2 safe keyword (which disables automatic encoding).
3. If you display HTML content on your website, clean it first and place it in a sandbox
DOMPurify
You can sanitize HTML (remove dangerous parts such as scripts) using DOMPurify library, which you can read more about here.
IFrame Sandbox
As an additional protection, it is advisable to display the sanitized HTML within a sandboxed (i.e., restricted within a sandbox) iframe. You can read more about it here.
Example Implementation
function sandboxAndPurifyUntrustedHtml(untrustedHtml) {
// Clean up untrusted HTML with DomPurify
var purifiedHtml = DOMPurify.sanitize(untrustedHtml);
// Create a sandbox iframe
var iframe = document.createElement('iframe');
iframe.sandbox = '';
document.body.appendChild(iframe);
iframe.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(purifiedHtml);
}
4. Serve all downloads with an appropriate Content-Disposition header to prevent rendering of user-submitted HTML/SVG within your origin
When serving files (downloads) from the server to users, send them with the Content-Disposition header, which indicates that it is an attachment. This way, the file is not directly visible in the end user's browser but is downloaded as a file to the disk, preventing XSS vulnerability in situations where the user might have downloaded an HTML or SVG file into an application that can then be loaded back from a URL address.
Content-Disposition: attachment; filename="document.pdf"
5. Beware of dynamic URL addresses in links
It is generally known that the URL address can be in the format of https://, http://, or start with just a forward slash, in which case the URL points to a path on the application's own address.
<a href="https://www.hakatemia.fi">Normal link</a>
It is less known that a URL address can contain, for example, JavaScript code that is executed when the link is clicked.
<a href="javascript:alert('xss')">XSS link</a>
This is why it is recommended to display links that have an HREF from outside the application with a secure component that can defensively display only URL addresses starting with https://, http://, or with a slash (/).
const SafeLink = ({ href, children }) => {
const isValidLink = href.startsWith('https://') || href.startsWith('http://') || href.startsWith('/');
if (isValidLink) {
return <a href={href}>{children}</a>;
} else {
return <p>Invalid link</p>;
}
};
6. Do not pass untrusted data to executed JavaScript functions/properties, such as eval, setTimeout, or innerHTML
With JavaScript, it is possible to call functions and modify the attributes of HTML elements that accept text (string) that is executed directly as code in the browser.
It is extremely important not to give any user input or other information coming from external sources to such attributes or functions at all.
7. Use only reputable, widely used dependencies and keep them up to date
One way to introduce an XSS vulnerability into an application is to use a JavaScript library that has an XSS vulnerability.
dependabot
If you use GitHub, you can automatically monitor vulnerabilities in your used node dependencies with Dependabot. Read more about Dependabot here.
npm audit
You can also use the "npm audit" tool to check for vulnerabilities in node dependencies. Learn more about the npm audit tool here.
retire.js
The Retire.js tool is able to find the JavaScript libraries used by your application and search for known vulnerabilities in them. Read more about the retire.js tool here.
Ready to become an ethical hacker?
Start today.
As a member of Hakatemia you get unlimited access to Hakatemia modules, exercises and tools, and you get access to the Hakatemia Discord channel where you can ask for help from both instructors and other Hakatemia members.