Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
40 prioritized NestJS best practices across architecture, DI, security, performance, testing, and microservices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
rules/security-sanitize-output.md
1---2title: Sanitize Output to Prevent XSS3impact: HIGH4impactDescription: XSS vulnerabilities can compromise user sessions and data5tags: security, xss, sanitization, html6---78## Sanitize Output to Prevent XSS910While 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.1112**Incorrect (storing raw HTML without sanitization):**1314```typescript15// Store raw HTML from users16@Injectable()17export class CommentsService {18async create(dto: CreateCommentDto): Promise<Comment> {19// User can inject: <script>steal(document.cookie)</script>20return this.repo.save({21content: dto.content, // Raw, unsanitized22authorId: dto.authorId,23});24}25}2627// Return HTML without sanitization28@Controller('pages')29export class PagesController {30@Get(':slug')31@Header('Content-Type', 'text/html')32async getPage(@Param('slug') slug: string): Promise<string> {33const page = await this.pagesService.findBySlug(slug);34// If page.content contains user input, XSS is possible35return `<html><body>${page.content}</body></html>`;36}37}3839// Reflect user input in errors40@Get(':id')41async findOne(@Param('id') id: string): Promise<User> {42const user = await this.repo.findOne({ where: { id } });43if (!user) {44// XSS if id contains malicious content and error is rendered45throw new NotFoundException(`User ${id} not found`);46}47return user;48}49```5051**Correct (sanitize content and use proper headers):**5253```typescript54// Sanitize HTML content before storage55import * as sanitizeHtml from 'sanitize-html';5657@Injectable()58export class CommentsService {59private readonly sanitizeOptions: sanitizeHtml.IOptions = {60allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],61allowedAttributes: {62a: ['href', 'title'],63},64allowedSchemes: ['http', 'https', 'mailto'],65};6667async create(dto: CreateCommentDto): Promise<Comment> {68return this.repo.save({69content: sanitizeHtml(dto.content, this.sanitizeOptions),70authorId: dto.authorId,71});72}73}7475// Use validation pipe to strip HTML76import { Transform } from 'class-transformer';7778export class CreatePostDto {79@IsString()80@MaxLength(1000)81@Transform(({ value }) => sanitizeHtml(value, { allowedTags: [] }))82title: string;8384@IsString()85@Transform(({ value }) =>86sanitizeHtml(value, {87allowedTags: ['p', 'br', 'b', 'i', 'a'],88allowedAttributes: { a: ['href'] },89}),90)91content: string;92}9394// Set proper Content-Type headers95@Controller('api')96export class ApiController {97@Get('data')98@Header('Content-Type', 'application/json')99async getData(): Promise<DataResponse> {100// JSON response - browser won't execute scripts101return this.service.getData();102}103}104105// Sanitize error messages106@Get(':id')107async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {108const user = await this.repo.findOne({ where: { id } });109if (!user) {110// UUID validation ensures safe format111throw new NotFoundException('User not found');112}113return user;114}115116// Use Helmet for CSP headers117import helmet from 'helmet';118119async function bootstrap() {120const app = await NestFactory.create(AppModule);121122app.use(123helmet({124contentSecurityPolicy: {125directives: {126defaultSrc: ["'self'"],127scriptSrc: ["'self'"],128styleSrc: ["'self'", "'unsafe-inline'"],129imgSrc: ["'self'", 'data:', 'https:'],130},131},132}),133);134135await app.listen(3000);136}137```138139Reference: [OWASP XSS Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)140