feat: Expand fleet to 23 skills across all domains
New skills (14): - nestjs-best-practices: 40 priority-ranked rules (kadajett) - fastapi: Pydantic v2, async SQLAlchemy, JWT auth (jezweb) - architecture-patterns: Clean Architecture, Hexagonal, DDD (wshobson) - python-performance-optimization: Profiling and optimization (wshobson) - ai-sdk: Vercel AI SDK streaming and agent patterns (vercel) - create-agent: Modular agent architecture with OpenRouter (openrouterteam) - proactive-agent: WAL Protocol, compaction recovery, self-improvement (halthelobster) - brand-guidelines: Brand identity enforcement (anthropics) - ui-animation: Motion design with accessibility (mblode) - marketing-ideas: 139 ideas across 14 categories (coreyhaines31) - pricing-strategy: SaaS pricing and tier design (coreyhaines31) - programmatic-seo: SEO at scale with playbooks (coreyhaines31) - competitor-alternatives: Comparison page architecture (coreyhaines31) - referral-program: Referral and affiliate programs (coreyhaines31) README reorganized by domain: Code Quality, Frontend, Backend, Auth, AI/Agent Building, Marketing, Design, Meta. Mosaic Stack is not limited to coding — the Orchestrator serves coding, business, design, marketing, writing, logistics, and analysis. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
139
skills/nestjs-best-practices/rules/security-sanitize-output.md
Normal file
139
skills/nestjs-best-practices/rules/security-sanitize-output.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user