Files
pos-system/docs/en/skills/security.md
Ho Ngoc Hai 9b6c585f57 Enhance documentation structure and improve bilingual support across skills
- 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.
2026-01-01 07:35:44 +07:00

21 KiB

name, description
name description
security 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// ❌ 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

// 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