- Updated skill documentation files to include structured metadata for better organization. - Enhanced bilingual descriptions and guidelines for clarity in both English and Vietnamese. - Refined sections on usage, best practices, and related skills to ensure consistency across all documentation. - Improved formatting and removed outdated references to streamline the documentation experience. - Added best practices checklists to relevant skills for better usability and adherence to standards.
797 lines
21 KiB
Markdown
797 lines
21 KiB
Markdown
---
|
|
name: security
|
|
description: Security best practices and patterns for GoodGo microservices platform. Use when implementing authentication, authorization, data protection, input validation, rate limiting, secrets management, or security testing across all services.
|
|
---
|
|
|
|
# Security Patterns for GoodGo Microservices
|
|
|
|
## When to Use This Skill
|
|
|
|
Use this skill when:
|
|
- Implementing authentication and authorization in any service
|
|
- Protecting sensitive data (PII, credentials, tokens)
|
|
- Validating user inputs and file uploads
|
|
- Implementing rate limiting and DDoS protection
|
|
- Setting up audit logging and security monitoring
|
|
- Encrypting data at rest and in transit
|
|
- Managing secrets and credentials
|
|
- Implementing security testing
|
|
- Handling security incidents
|
|
- Designing secure API endpoints
|
|
|
|
## Core Security Principles
|
|
|
|
1. **Defense in Depth**: Multiple layers of security controls
|
|
2. **Least Privilege**: Grant minimum required permissions
|
|
3. **Fail Secure**: Default to deny access
|
|
4. **Separation of Duties**: Critical operations require multiple approvals
|
|
5. **Audit Everything**: Log all security-relevant events
|
|
6. **Encrypt Sensitive Data**: PII, tokens, credentials must be encrypted
|
|
7. **Validate All Inputs**: Never trust user input
|
|
8. **Principle of Least Exposure**: Minimize attack surface
|
|
9. **Secure by Default**: Security built-in, not bolted on
|
|
10. **Assume Breach**: Design for detection and response
|
|
|
|
## Authentication & Authorization
|
|
|
|
### JWT Token Validation
|
|
|
|
```typescript
|
|
// src/middlewares/auth.middleware.ts
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import { jwtService } from '@goodgo/auth-sdk';
|
|
import { logger } from '@goodgo/logger';
|
|
|
|
export const authenticate = () => {
|
|
return async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
// Extract token from Authorization header or cookie
|
|
let token: string | null = null;
|
|
|
|
const authHeader = req.headers.authorization;
|
|
if (authHeader?.startsWith('Bearer ')) {
|
|
token = authHeader.substring(7);
|
|
} else if (req.cookies?.access_token) {
|
|
token = req.cookies.access_token;
|
|
}
|
|
|
|
if (!token) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
error: { code: 'AUTH_REQUIRED', message: 'Authentication required' }
|
|
});
|
|
}
|
|
|
|
// Verify token
|
|
const payload = await jwtService.verifyAccessToken(token);
|
|
|
|
// Attach user to request
|
|
req.user = {
|
|
id: payload.sub,
|
|
userId: payload.sub,
|
|
email: payload.email,
|
|
roles: payload.roles || [],
|
|
permissions: payload.permissions || []
|
|
};
|
|
|
|
next();
|
|
} catch (error) {
|
|
logger.warn('Authentication failed', { error: error.message });
|
|
return res.status(401).json({
|
|
success: false,
|
|
error: { code: 'INVALID_TOKEN', message: 'Invalid or expired token' }
|
|
});
|
|
}
|
|
};
|
|
};
|
|
```
|
|
|
|
### Role-Based Authorization
|
|
|
|
```typescript
|
|
// src/middlewares/rbac.middleware.ts
|
|
export const requireRole = (...allowedRoles: string[]) => {
|
|
return (req: Request, res: Response, next: NextFunction) => {
|
|
if (!req.user) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
error: { code: 'AUTH_REQUIRED', message: 'Authentication required' }
|
|
});
|
|
}
|
|
|
|
const userRoles = req.user.roles || [];
|
|
const hasRole = userRoles.some(role => allowedRoles.includes(role));
|
|
|
|
if (!hasRole) {
|
|
logger.warn('Access denied - insufficient role', {
|
|
userId: req.user.id,
|
|
userRoles,
|
|
requiredRoles: allowedRoles
|
|
});
|
|
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: { code: 'FORBIDDEN', message: 'Insufficient permissions' }
|
|
});
|
|
}
|
|
|
|
next();
|
|
};
|
|
};
|
|
|
|
// Permission-based authorization
|
|
export const requirePermission = (resource: string, action: string) => {
|
|
return async (req: Request, res: Response, next: NextFunction) => {
|
|
if (!req.user) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
error: { code: 'AUTH_REQUIRED', message: 'Authentication required' }
|
|
});
|
|
}
|
|
|
|
const permission = `${resource}:${action}`;
|
|
const hasPermission = req.user.permissions?.includes(permission);
|
|
|
|
if (!hasPermission) {
|
|
logger.warn('Access denied - insufficient permission', {
|
|
userId: req.user.id,
|
|
required: permission
|
|
});
|
|
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: { code: 'FORBIDDEN', message: 'Insufficient permissions' }
|
|
});
|
|
}
|
|
|
|
next();
|
|
};
|
|
};
|
|
|
|
// Usage in routes
|
|
router.post(
|
|
'/api/v1/users',
|
|
authenticate(),
|
|
requirePermission('users', 'create'),
|
|
userController.create
|
|
);
|
|
```
|
|
|
|
### Resource Ownership Validation
|
|
|
|
```typescript
|
|
// Ensure users can only access their own resources
|
|
export const requireOwnership = (resourceIdParam: string = 'id') => {
|
|
return (req: Request, res: Response, next: NextFunction) => {
|
|
const resourceId = req.params[resourceIdParam];
|
|
const userId = req.user?.id;
|
|
|
|
if (resourceId !== userId) {
|
|
logger.warn('Access denied - resource ownership mismatch', {
|
|
userId,
|
|
resourceId
|
|
});
|
|
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: { code: 'FORBIDDEN', message: 'Access denied' }
|
|
});
|
|
}
|
|
|
|
next();
|
|
};
|
|
};
|
|
```
|
|
|
|
## Data Protection
|
|
|
|
### Encryption Service
|
|
|
|
```typescript
|
|
// src/core/security/encryption.service.ts
|
|
import crypto from 'crypto';
|
|
|
|
const ALGORITHM = 'aes-256-gcm';
|
|
const IV_LENGTH = 16;
|
|
const TAG_LENGTH = 16;
|
|
|
|
export class EncryptionService {
|
|
private getKey(): Buffer {
|
|
const secret = process.env.ENCRYPTION_KEY;
|
|
if (!secret || secret.length < 32) {
|
|
throw new Error('ENCRYPTION_KEY must be at least 32 characters');
|
|
}
|
|
return crypto.scryptSync(secret, 'salt', 32);
|
|
}
|
|
|
|
encrypt(text: string): string {
|
|
const key = this.getKey();
|
|
const iv = crypto.randomBytes(IV_LENGTH);
|
|
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
|
|
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
encrypted += cipher.final('hex');
|
|
const tag = cipher.getAuthTag();
|
|
|
|
return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted}`;
|
|
}
|
|
|
|
decrypt(encryptedText: string): string {
|
|
const [ivHex, tagHex, encrypted] = encryptedText.split(':');
|
|
const iv = Buffer.from(ivHex, 'hex');
|
|
const tag = Buffer.from(tagHex, 'hex');
|
|
|
|
const key = this.getKey();
|
|
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
decipher.setAuthTag(tag);
|
|
|
|
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
decrypted += decipher.final('utf8');
|
|
return decrypted;
|
|
}
|
|
}
|
|
|
|
// Usage: Encrypt PII before storing
|
|
const encryption = new EncryptionService();
|
|
const encryptedPhone = encryption.encrypt(user.phone);
|
|
```
|
|
|
|
### Password Hashing
|
|
|
|
```typescript
|
|
// Always use bcrypt with appropriate cost factor
|
|
import bcrypt from 'bcrypt';
|
|
|
|
const SALT_ROUNDS = 12; // Production: 12, Development: 10
|
|
|
|
export class PasswordService {
|
|
async hash(password: string): Promise<string> {
|
|
return bcrypt.hash(password, SALT_ROUNDS);
|
|
}
|
|
|
|
async verify(password: string, hash: string): Promise<boolean> {
|
|
return bcrypt.compare(password, hash);
|
|
}
|
|
|
|
// Never log passwords
|
|
sanitizeForLogging(data: any): any {
|
|
const sanitized = { ...data };
|
|
if (sanitized.password) sanitized.password = '[REDACTED]';
|
|
if (sanitized.passwordHash) sanitized.passwordHash = '[REDACTED]';
|
|
return sanitized;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Token Hashing
|
|
|
|
```typescript
|
|
// Hash tokens before storing in database
|
|
import crypto from 'crypto';
|
|
|
|
export class TokenService {
|
|
hashToken(token: string): string {
|
|
const salt = process.env.TOKEN_SALT || 'default-salt-change-in-production';
|
|
return crypto
|
|
.createHash('sha256')
|
|
.update(token + salt)
|
|
.digest('hex');
|
|
}
|
|
|
|
generateSecureToken(length: number = 32): string {
|
|
return crypto.randomBytes(length).toString('hex');
|
|
}
|
|
}
|
|
```
|
|
|
|
## Input Validation
|
|
|
|
### Zod Schema Validation
|
|
|
|
```typescript
|
|
// Always validate inputs with Zod
|
|
import { z } from 'zod';
|
|
|
|
// DTO with validation
|
|
export const CreateUserDto = z.object({
|
|
email: z.string().email('Invalid email format'),
|
|
password: z.string()
|
|
.min(8, 'Password must be at least 8 characters')
|
|
.regex(/[A-Z]/, 'Password must contain uppercase letter')
|
|
.regex(/[a-z]/, 'Password must contain lowercase letter')
|
|
.regex(/[0-9]/, 'Password must contain number')
|
|
.regex(/[^A-Za-z0-9]/, 'Password must contain special character'),
|
|
phone: z.string()
|
|
.regex(/^\+[1-9]\d{1,14}$/, 'Invalid phone format (E.164)')
|
|
.optional(),
|
|
name: z.string().min(1).max(255)
|
|
});
|
|
|
|
// In controller
|
|
export class UserController {
|
|
async create(req: Request, res: Response) {
|
|
try {
|
|
const dto = CreateUserDto.parse(req.body);
|
|
const user = await this.service.create(dto);
|
|
res.status(201).json({ success: true, data: user });
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: {
|
|
code: 'VALIDATION_ERROR',
|
|
message: 'Invalid input data',
|
|
details: error.errors
|
|
}
|
|
});
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### File Upload Validation
|
|
|
|
```typescript
|
|
// Validate file uploads
|
|
import fileType from 'file-type';
|
|
|
|
export class FileValidationService {
|
|
private readonly MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
private readonly ALLOWED_TYPES = ['image/jpeg', 'image/png', 'application/pdf'];
|
|
|
|
async validateFile(file: Express.Multer.File): Promise<void> {
|
|
// Size check
|
|
if (file.size > this.MAX_FILE_SIZE) {
|
|
throw new HttpError(400, 'FILE_TOO_LARGE', 'File exceeds maximum size');
|
|
}
|
|
|
|
// Type check
|
|
if (!this.ALLOWED_TYPES.includes(file.mimetype)) {
|
|
throw new HttpError(400, 'INVALID_FILE_TYPE', 'File type not allowed');
|
|
}
|
|
|
|
// Content validation (prevent MIME type spoofing)
|
|
const type = await fileType.fromBuffer(file.buffer);
|
|
if (!type || !this.ALLOWED_TYPES.includes(type.mime)) {
|
|
throw new HttpError(400, 'INVALID_FILE_CONTENT', 'File content mismatch');
|
|
}
|
|
|
|
// TODO: Add virus scanning for production
|
|
}
|
|
}
|
|
```
|
|
|
|
### SQL Injection Prevention
|
|
|
|
```typescript
|
|
// Always use Prisma parameterized queries (automatic)
|
|
// Never use string concatenation for queries
|
|
|
|
// ❌ BAD - Never do this
|
|
const query = `SELECT * FROM users WHERE email = '${email}'`;
|
|
|
|
// ✅ GOOD - Use Prisma
|
|
const user = await prisma.user.findUnique({
|
|
where: { email }
|
|
});
|
|
|
|
// ✅ GOOD - For dynamic queries
|
|
const where: any = {};
|
|
if (email) where.email = email;
|
|
if (status) where.status = status;
|
|
|
|
const users = await prisma.user.findMany({ where });
|
|
```
|
|
|
|
## Rate Limiting
|
|
|
|
```typescript
|
|
// Implement rate limiting for all endpoints
|
|
import rateLimit from 'express-rate-limit';
|
|
import RedisStore from 'rate-limit-redis';
|
|
import Redis from 'ioredis';
|
|
|
|
const redis = new Redis(process.env.REDIS_URL);
|
|
|
|
// Standard rate limit
|
|
export const standardLimiter = rateLimit({
|
|
store: new RedisStore({
|
|
client: redis,
|
|
prefix: 'rl:standard:'
|
|
}),
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 100, // 100 requests per window
|
|
message: 'Too many requests, please try again later',
|
|
standardHeaders: true,
|
|
legacyHeaders: false
|
|
});
|
|
|
|
// Strict rate limit for sensitive operations
|
|
export const strictLimiter = rateLimit({
|
|
store: new RedisStore({
|
|
client: redis,
|
|
prefix: 'rl:strict:'
|
|
}),
|
|
windowMs: 60 * 60 * 1000, // 1 hour
|
|
max: 10,
|
|
message: 'Rate limit exceeded for this operation'
|
|
});
|
|
|
|
// Login-specific rate limit
|
|
export const loginLimiter = rateLimit({
|
|
store: new RedisStore({
|
|
client: redis,
|
|
prefix: 'rl:login:'
|
|
}),
|
|
windowMs: 15 * 60 * 1000,
|
|
max: 5, // 5 login attempts per 15 minutes
|
|
skipSuccessfulRequests: true,
|
|
message: 'Too many login attempts, please try again later'
|
|
});
|
|
|
|
// Usage
|
|
router.post('/api/v1/auth/login', loginLimiter, authController.login);
|
|
router.post('/api/v1/users', authenticate(), strictLimiter, userController.create);
|
|
```
|
|
|
|
## Error Handling Security
|
|
|
|
```typescript
|
|
// Sanitize error messages to prevent information disclosure
|
|
export class SecureErrorHandler {
|
|
handleError(error: Error, req: Request, res: Response) {
|
|
const isDev = process.env.NODE_ENV === 'development';
|
|
const isProd = process.env.NODE_ENV === 'production';
|
|
|
|
// Log full error internally
|
|
logger.error('Request error', {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
path: req.path,
|
|
method: req.method,
|
|
userId: req.user?.id
|
|
});
|
|
|
|
// Don't expose user existence
|
|
if (error.message.includes('user not found') ||
|
|
error.message.includes('invalid credentials')) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
error: {
|
|
code: 'INVALID_CREDENTIALS',
|
|
message: 'Invalid email or password'
|
|
}
|
|
});
|
|
}
|
|
|
|
// Validation errors - safe to expose
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: {
|
|
code: 'VALIDATION_ERROR',
|
|
message: 'Invalid input data',
|
|
details: error.errors
|
|
}
|
|
});
|
|
}
|
|
|
|
// Generic errors for production
|
|
if (isProd) {
|
|
return res.status(500).json({
|
|
success: false,
|
|
error: {
|
|
code: 'INTERNAL_ERROR',
|
|
message: 'An error occurred. Please try again later.'
|
|
}
|
|
});
|
|
}
|
|
|
|
// Detailed errors only in development
|
|
return res.status(500).json({
|
|
success: false,
|
|
error: {
|
|
code: 'INTERNAL_ERROR',
|
|
message: error.message,
|
|
stack: isDev ? error.stack : undefined
|
|
}
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
## Secrets Management
|
|
|
|
```typescript
|
|
// Never hardcode secrets
|
|
// Always use environment variables with validation
|
|
import { z } from 'zod';
|
|
|
|
const secretsSchema = z.object({
|
|
JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
|
|
JWT_REFRESH_SECRET: z.string().min(32),
|
|
DATABASE_URL: z.string().url(),
|
|
REDIS_URL: z.string().url().optional(),
|
|
ENCRYPTION_KEY: z.string().min(32).optional()
|
|
});
|
|
|
|
export const secrets = secretsSchema.parse(process.env);
|
|
|
|
// For production, use secret management:
|
|
// - AWS Secrets Manager
|
|
// - HashiCorp Vault
|
|
// - Kubernetes Secrets
|
|
// - Azure Key Vault
|
|
|
|
// Rotate secrets regularly (quarterly recommended)
|
|
```
|
|
|
|
## Audit Logging
|
|
|
|
```typescript
|
|
// Log all security-relevant events
|
|
export class AuditService {
|
|
async logSecurityEvent(
|
|
event: string,
|
|
userId: string | null,
|
|
details: Record<string, any>,
|
|
req?: Request
|
|
) {
|
|
await this.prisma.auditLog.create({
|
|
data: {
|
|
event,
|
|
userId,
|
|
type: 'SECURITY',
|
|
details: this.sanitizeDetails(details),
|
|
ipAddress: req?.ip || details.ipAddress,
|
|
userAgent: req?.get('user-agent'),
|
|
timestamp: new Date()
|
|
}
|
|
});
|
|
}
|
|
|
|
// Sanitize PII from logs
|
|
private sanitizeDetails(details: Record<string, any>): Record<string, any> {
|
|
const sensitive = ['password', 'token', 'secret', 'ssn', 'creditCard'];
|
|
const sanitized = { ...details };
|
|
|
|
for (const key of sensitive) {
|
|
if (sanitized[key]) {
|
|
sanitized[key] = '[REDACTED]';
|
|
}
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
await auditService.logSecurityEvent('LOGIN_SUCCESS', user.id, {
|
|
email: user.email,
|
|
ipAddress: req.ip
|
|
}, req);
|
|
|
|
await auditService.logSecurityEvent('PERMISSION_DENIED', user.id, {
|
|
resource: 'users',
|
|
action: 'delete',
|
|
targetId: targetUserId
|
|
}, req);
|
|
```
|
|
|
|
## Security Headers
|
|
|
|
```typescript
|
|
// Add security headers middleware
|
|
import helmet from 'helmet';
|
|
|
|
app.use(helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
scriptSrc: ["'self'"],
|
|
imgSrc: ["'self'", "data:", "https:"]
|
|
}
|
|
},
|
|
hsts: {
|
|
maxAge: 31536000,
|
|
includeSubDomains: true,
|
|
preload: true
|
|
}
|
|
}));
|
|
|
|
// Additional headers
|
|
app.use((req, res, next) => {
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
res.setHeader('X-Frame-Options', 'DENY');
|
|
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
next();
|
|
});
|
|
```
|
|
|
|
## CORS Configuration
|
|
|
|
```typescript
|
|
// Configure CORS securely
|
|
import cors from 'cors';
|
|
|
|
const allowedOrigins = process.env.CORS_ORIGIN?.split(',') || [];
|
|
|
|
app.use(cors({
|
|
origin: (origin, callback) => {
|
|
if (!origin || allowedOrigins.includes(origin)) {
|
|
callback(null, true);
|
|
} else {
|
|
callback(new Error('Not allowed by CORS'));
|
|
}
|
|
},
|
|
credentials: true,
|
|
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
exposedHeaders: ['X-Request-ID'],
|
|
maxAge: 86400 // 24 hours
|
|
}));
|
|
```
|
|
|
|
## Security Testing
|
|
|
|
```typescript
|
|
// Security test patterns
|
|
describe('Security Tests', () => {
|
|
it('should prevent SQL injection', async () => {
|
|
const maliciousInput = "'; DROP TABLE users; --";
|
|
const response = await request(app)
|
|
.get(`/api/v1/users?search=${encodeURIComponent(maliciousInput)}`)
|
|
.set('Authorization', `Bearer ${token}`);
|
|
|
|
expect(response.status).not.toBe(500);
|
|
// Should return 400 or empty results, not crash
|
|
});
|
|
|
|
it('should prevent XSS attacks', async () => {
|
|
const xssPayload = '<script>alert("XSS")</script>';
|
|
const response = await request(app)
|
|
.post('/api/v1/users')
|
|
.send({ email: xssPayload, password: 'test123' });
|
|
|
|
// Response should sanitize or reject
|
|
expect(response.body.data?.email).not.toContain('<script>');
|
|
});
|
|
|
|
it('should enforce authentication', async () => {
|
|
const response = await request(app)
|
|
.get('/api/v1/users');
|
|
|
|
expect(response.status).toBe(401);
|
|
});
|
|
|
|
it('should enforce authorization', async () => {
|
|
const userToken = await createUserToken({ roles: ['user'] });
|
|
const response = await request(app)
|
|
.delete('/api/v1/users/123')
|
|
.set('Authorization', `Bearer ${userToken}`);
|
|
|
|
expect(response.status).toBe(403);
|
|
});
|
|
|
|
it('should rate limit excessive requests', async () => {
|
|
const requests = Array(20).fill(null).map(() =>
|
|
request(app).get('/api/v1/users')
|
|
);
|
|
|
|
const responses = await Promise.all(requests);
|
|
const rateLimited = responses.filter(r => r.status === 429);
|
|
|
|
expect(rateLimited.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Security Checklist
|
|
|
|
Before deploying any service:
|
|
|
|
- [ ] All endpoints require authentication (except public)
|
|
- [ ] Authorization checks implemented (RBAC/ABAC)
|
|
- [ ] Input validation with Zod schemas
|
|
- [ ] Rate limiting configured
|
|
- [ ] Error messages sanitized (no info disclosure)
|
|
- [ ] PII encrypted at rest
|
|
- [ ] Passwords hashed with bcrypt (cost 12+)
|
|
- [ ] Tokens hashed before storing
|
|
- [ ] Secrets in environment variables (never hardcoded)
|
|
- [ ] HTTPS enforced (TLS 1.2+)
|
|
- [ ] CORS configured correctly
|
|
- [ ] Security headers set (helmet)
|
|
- [ ] Audit logging enabled
|
|
- [ ] SQL injection prevented (use Prisma)
|
|
- [ ] XSS prevention (input sanitization)
|
|
- [ ] File upload validation
|
|
- [ ] Security tests passing
|
|
- [ ] Dependencies scanned for vulnerabilities
|
|
- [ ] Secrets rotation plan in place
|
|
|
|
## Common Security Anti-Patterns
|
|
|
|
```typescript
|
|
// ❌ BAD: Hardcoded secrets
|
|
const SECRET = 'my-secret-key';
|
|
|
|
// ✅ GOOD: Environment variables
|
|
const SECRET = process.env.JWT_SECRET;
|
|
|
|
// ❌ BAD: Plain text passwords
|
|
await prisma.user.create({ data: { password: password } });
|
|
|
|
// ✅ GOOD: Hashed passwords
|
|
await prisma.user.create({
|
|
data: { passwordHash: await bcrypt.hash(password, 12) }
|
|
});
|
|
|
|
// ❌ BAD: Exposing user existence
|
|
if (!user) {
|
|
throw new Error('User not found'); // Reveals user doesn't exist
|
|
}
|
|
|
|
// ✅ GOOD: Generic error
|
|
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
|
|
throw new Error('Invalid credentials');
|
|
}
|
|
|
|
// ❌ BAD: No input validation
|
|
const email = req.body.email;
|
|
|
|
// ✅ GOOD: Validate with Zod
|
|
const { email } = CreateUserDto.parse(req.body);
|
|
|
|
// ❌ BAD: Stack traces in production
|
|
res.status(500).json({ error: error.stack });
|
|
|
|
// ✅ GOOD: Sanitized errors
|
|
res.status(500).json({
|
|
error: { code: 'INTERNAL_ERROR', message: 'An error occurred' }
|
|
});
|
|
```
|
|
|
|
## Incident Response
|
|
|
|
```typescript
|
|
// Security incident detection and response
|
|
export class SecurityIncidentService {
|
|
async detectAnomaly(userId: string, event: string, context: any) {
|
|
// Check for suspicious patterns
|
|
const recentEvents = await this.getRecentEvents(userId, '1h');
|
|
|
|
if (recentEvents.length > 10) {
|
|
await this.triggerAlert('SUSPICIOUS_ACTIVITY', {
|
|
userId,
|
|
eventCount: recentEvents.length,
|
|
timeWindow: '1h'
|
|
});
|
|
}
|
|
|
|
// Check for privilege escalation attempts
|
|
if (event === 'PERMISSION_DENIED' && context.requiredPermission) {
|
|
await this.logSecurityEvent('PRIVILEGE_ESCALATION_ATTEMPT', userId, context);
|
|
}
|
|
}
|
|
|
|
async triggerAlert(type: string, details: any) {
|
|
// Send to monitoring system
|
|
logger.error('Security alert', { type, details });
|
|
|
|
// TODO: Integrate with PagerDuty, Slack, etc.
|
|
}
|
|
}
|
|
```
|
|
|
|
## Resources
|
|
|
|
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
|
- [OWASP API Security Top 10](https://owasp.org/www-project-api-security/)
|
|
- [Node.js Security Best Practices](https://nodejs.org/en/docs/guides/security/)
|
|
- [Express Security Best Practices](https://expressjs.com/en/advanced/best-practice-security.html)
|