Stored Cross-Site-Scripting (XSS)
11-01-2025 | 3 min read
After an annual round of penetration testing at the company I work for, the web application was found to be vulnerable to a Stored Cross-Site Scripting (XSS) attack.
Understanding Stored XSS
A Stored Cross-Site Scripting (XSS) attack occurs when a web application accepts user input, stores it on the server, and later displays it on a webpage without proper sanitisation, allowing malicious scripts to execute in a victim's browser. This differs from Reflected XSS, where the payload is not stored but rather executed immediately when the malicious link is clicked.
To test for this vulnerability, we used PortSwigger's HTTP traffic interceptor, Burp Proxy. This allowed us to intercept the request, inject a malicious script into a parameter, and observe its execution when the data was rendered on the web page.
Example Payloads
Consider the streetAddress
parameter, which is sent to the server as part of an HTTP POST request:
Before Injection:
<input type="hidden" name="streetAddress" value="10 Example Street">
After Injection:
<input type="hidden" name="streetAddress" value="10 Example Street"><script>alert('XSS');</script>">
In this example, the injected script executes a simple alert('XSS')
, demonstrating the potential for arbitrary JavaScript execution. A real attacker could replace this with a script to steal session cookies, perform keylogging, or carry out other malicious actions.
The Solution
Mitigating Stored XSS requires a multi-layered approach involving input validation, output encoding, and security headers.
1. Input Validation
Input validation ensures that only expected characters are allowed in user inputs. Where possible, use whitelisting or regular expressions (regex) to enforce constraints. For example, an account number field should only permit numeric characters.
2. Output Encoding (Primary Defence)
Output encoding ensures that special characters are displayed as plain text rather than executed as code. This should be implemented according to the context:
- HTML Encoding: Convert
<script>
into<script>
. - JavaScript Encoding: Escape quotes and special characters.
- Attribute Encoding: Prevent breaking out of attributes.
3. Secure Coding Implementation
We used the Validator library to sanitise input, escaping special characters using .escape()
, as shown below:
const { streetAddress, city } = req.body;
const validator = require('validator');
let sanitizedAddress = streetAddress ? validator.escape(streetAddress) : streetAddress;
let sanitizedCity = city ? validator.escape(city) : city;
if ((sanitizedAddress !== streetAddress) || (sanitizedCity !== city)) {
return reject(new Error("Unable to update details due to sanitisation error."));
}
req.body.street = sanitizedAddress || streetAddress;
This ensures that special characters such as <, >, &, ', " and /
are escaped before storage, preventing execution when displayed.
4. Additional Security Best Practices
Use a Content Security Policy (CSP)
A strong Content Security Policy (CSP) header can prevent the execution of inline scripts:
Content-Security-Policy: default-src 'self'; script-src 'self'
Set HTTPOnly and Secure Flags on Cookies
This prevents scripts from accessing cookies, reducing the risk of session hijacking:
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict
Conclusion
By implementing input validation, output encoding, and security headers, we effectively mitigated the risk of Stored XSS in our application. While validation is a good first step, output encoding remains the primary defence against XSS. Additionally, adopting CSP and secure cookie settings further hardens the application's security against such attacks.
Get in touch if you have any feedback or questions!