Introduction
Sanitization transforms input to remove or escape dangerous content. While validation rejects bad input, sanitization cleans it. This is essential for user-generated content that will be stored and displayed.
Key Concepts
- HTML Sanitization: Remove/escape HTML tags to prevent XSS
- SQL Escaping: Handled by ORM/query builders (parameterized queries)
- Path Sanitization: Prevent directory traversal attacks
- Transformation: class-transformer decorators for cleaning input
Real World Context
Sanitization scenarios:
- User bio fields displayed on profile pages
- Comments and reviews shown to other users
- File uploads with user-provided names
- Search queries displayed in results
Deep Dive
HTML/XSS Sanitization
Install sanitize-html to strip dangerous HTML tags while preserving safe formatting.
bashnpm install sanitize-html npm install -D @types/sanitize-html
Wrap it in a reusable class-transformer decorator so it can be applied declaratively to any DTO field.
typescriptimport * as sanitizeHtml from 'sanitize-html'; import { Transform } from 'class-transformer'; export function SanitizeHtml() { return Transform(({ value }) => { if (typeof value !== 'string') return value; return sanitizeHtml(value, { allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'], allowedAttributes: { a: ['href', 'title'], }, allowedSchemes: ['http', 'https'], }); }); } export class CreatePostDto { @IsString() @MaxLength(100) title: string; @IsString() @MaxLength(10000) @SanitizeHtml() content: string; }
The allowedSchemes option restricts links to http and https, blocking javascript: URIs that could execute code.
Strip HTML Completely
For fields that should never contain HTML (like comments or usernames), strip all tags by setting allowedTags to an empty array.
typescriptexport function StripHtml() { return Transform(({ value }) => { if (typeof value !== 'string') return value; return sanitizeHtml(value, { allowedTags: [], allowedAttributes: {}, }); }); } export class CommentDto { @IsString() @StripHtml() text: string; }
This is the safest option for plain-text fields — any <script>, <img onerror>, or other injection attempts are completely removed.
Trim and Normalize
Create transformer decorators for common cleaning operations like trimming whitespace and normalizing email case.
typescriptimport { Transform } from 'class-transformer'; export function Trim() { return Transform(({ value }) => typeof value === 'string' ? value.trim() : value, ); } export function NormalizeEmail() { return Transform(({ value }) => typeof value === 'string' ? value.toLowerCase().trim() : value, ); } export class LoginDto { @IsEmail() @NormalizeEmail() email: string; @IsString() @Trim() password: string; }
Normalizing emails to lowercase prevents users from accidentally creating duplicate accounts with different casing.
Path Sanitization
Sanitize user-provided filenames to prevent directory traversal attacks (e.g., ../../etc/passwd). Always validate the resolved path stays within your upload directory.
typescriptimport * as path from 'path'; export function sanitizeFilename(filename: string): string { // Remove path components const basename = path.basename(filename); // Remove dangerous characters return basename .replace(/[^a-zA-Z0-9._-]/g, '_') .replace(/\.{2,}/g, '.') .substring(0, 255); } @Injectable() export class FileService { async saveFile(file: Express.Multer.File, userFilename: string) { const safeName = sanitizeFilename(userFilename); const uniqueName = `${Date.now()}-${safeName}`; // Always use path.join with validated base directory const safePath = path.join(this.uploadDir, uniqueName); // Verify path is within allowed directory if (!safePath.startsWith(this.uploadDir)) { throw new BadRequestException('Invalid file path'); } await fs.writeFile(safePath, file.buffer); return uniqueName; } }
The safePath.startsWith(this.uploadDir) check is the critical line — it prevents path traversal even if the sanitization function misses an edge case.
SQL Injection Prevention
Never concatenate user input into SQL strings. Use parameterized queries or ORM methods that handle escaping automatically.
typescript// WRONG - Vulnerable to SQL injection const query = `SELECT * FROM users WHERE name = '${name}'`; // CORRECT - Parameterized query (TypeORM) const users = await this.userRepository .createQueryBuilder('user') .where('user.name = :name', { name }) .getMany(); // CORRECT - Repository method const user = await this.userRepository.findOne({ where: { name } });
Both correct approaches bind the name value as a parameter, so the database never interprets it as SQL syntax — even if it contains malicious content.
Common Pitfalls
- Sanitizing after storage: Sanitize before storing, not when displaying. Stored XSS is dangerous.
- Over-sanitization: Don't break legitimate content. Allow safe HTML when needed.
- Forgetting URL params: Query strings and path params need validation too.
Best Practices
- Sanitize user-generated content before storage
- Use parameterized queries (never string concatenation)
- Validate file paths are within allowed directories
- Create reusable transformer decorators
- Configure sanitize-html based on your content needs
Summary
- Sanitization cleans input to remove dangerous content, complementing validation which rejects it
- Use
sanitize-htmlfor HTML/XSS prevention andclass-transformerdecorators for field cleaning - Always use parameterized queries (via ORM) instead of string concatenation to prevent SQL injection
- Sanitize user-generated content before storage, not at display time
Code Examples
import * as sanitizeHtml from 'sanitize-html';
import { Transform } from 'class-transformer';
export function SanitizeHtml() {
return Transform(({ value }) => {
if (typeof value !== 'string') return value;
return sanitizeHtml(value, {
allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
allowedAttributes: {
a: ['href', 'title'],
},
allowedSchemes: ['http', 'https'],
});
});
}
export class CreatePostDto {
@IsString()
@MaxLength(100)
title: string;
@IsString()
@MaxLength(10000)
@SanitizeHtml()
content: string;
}