140 lines
3.7 KiB
Markdown
140 lines
3.7 KiB
Markdown
---
|
|
title: Sanitize Output to Prevent XSS
|
|
impact: HIGH
|
|
impactDescription: XSS vulnerabilities can compromise user sessions and data
|
|
tags: security, xss, sanitization, html
|
|
---
|
|
|
|
## Sanitize Output to Prevent XSS
|
|
|
|
While NestJS APIs typically return JSON (which browsers don't execute), XSS risks exist when rendering HTML, storing user content, or when frontend frameworks improperly handle API responses. Sanitize user-generated content before storage and use proper Content-Type headers.
|
|
|
|
**Incorrect (storing raw HTML without sanitization):**
|
|
|
|
```typescript
|
|
// Store raw HTML from users
|
|
@Injectable()
|
|
export class CommentsService {
|
|
async create(dto: CreateCommentDto): Promise<Comment> {
|
|
// User can inject: <script>steal(document.cookie)</script>
|
|
return this.repo.save({
|
|
content: dto.content, // Raw, unsanitized
|
|
authorId: dto.authorId,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Return HTML without sanitization
|
|
@Controller('pages')
|
|
export class PagesController {
|
|
@Get(':slug')
|
|
@Header('Content-Type', 'text/html')
|
|
async getPage(@Param('slug') slug: string): Promise<string> {
|
|
const page = await this.pagesService.findBySlug(slug);
|
|
// If page.content contains user input, XSS is possible
|
|
return `<html><body>${page.content}</body></html>`;
|
|
}
|
|
}
|
|
|
|
// Reflect user input in errors
|
|
@Get(':id')
|
|
async findOne(@Param('id') id: string): Promise<User> {
|
|
const user = await this.repo.findOne({ where: { id } });
|
|
if (!user) {
|
|
// XSS if id contains malicious content and error is rendered
|
|
throw new NotFoundException(`User ${id} not found`);
|
|
}
|
|
return user;
|
|
}
|
|
```
|
|
|
|
**Correct (sanitize content and use proper headers):**
|
|
|
|
```typescript
|
|
// Sanitize HTML content before storage
|
|
import * as sanitizeHtml from 'sanitize-html';
|
|
|
|
@Injectable()
|
|
export class CommentsService {
|
|
private readonly sanitizeOptions: sanitizeHtml.IOptions = {
|
|
allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
|
|
allowedAttributes: {
|
|
a: ['href', 'title'],
|
|
},
|
|
allowedSchemes: ['http', 'https', 'mailto'],
|
|
};
|
|
|
|
async create(dto: CreateCommentDto): Promise<Comment> {
|
|
return this.repo.save({
|
|
content: sanitizeHtml(dto.content, this.sanitizeOptions),
|
|
authorId: dto.authorId,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Use validation pipe to strip HTML
|
|
import { Transform } from 'class-transformer';
|
|
|
|
export class CreatePostDto {
|
|
@IsString()
|
|
@MaxLength(1000)
|
|
@Transform(({ value }) => sanitizeHtml(value, { allowedTags: [] }))
|
|
title: string;
|
|
|
|
@IsString()
|
|
@Transform(({ value }) =>
|
|
sanitizeHtml(value, {
|
|
allowedTags: ['p', 'br', 'b', 'i', 'a'],
|
|
allowedAttributes: { a: ['href'] },
|
|
}),
|
|
)
|
|
content: string;
|
|
}
|
|
|
|
// Set proper Content-Type headers
|
|
@Controller('api')
|
|
export class ApiController {
|
|
@Get('data')
|
|
@Header('Content-Type', 'application/json')
|
|
async getData(): Promise<DataResponse> {
|
|
// JSON response - browser won't execute scripts
|
|
return this.service.getData();
|
|
}
|
|
}
|
|
|
|
// Sanitize error messages
|
|
@Get(':id')
|
|
async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {
|
|
const user = await this.repo.findOne({ where: { id } });
|
|
if (!user) {
|
|
// UUID validation ensures safe format
|
|
throw new NotFoundException('User not found');
|
|
}
|
|
return user;
|
|
}
|
|
|
|
// Use Helmet for CSP headers
|
|
import helmet from 'helmet';
|
|
|
|
async function bootstrap() {
|
|
const app = await NestFactory.create(AppModule);
|
|
|
|
app.use(
|
|
helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
scriptSrc: ["'self'"],
|
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
imgSrc: ["'self'", 'data:', 'https:'],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
await app.listen(3000);
|
|
}
|
|
```
|
|
|
|
Reference: [OWASP XSS Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
|