Security Best Practices
This guide covers security best practices for building secure applications with MBC CQRS Serverless, including input validation, authentication, authorization, and data protection.
Input Validation
Validate All Input
Always validate input at the API boundary using class-validator decorators.
import {
IsNotEmpty,
IsString,
IsEmail,
IsOptional,
MaxLength,
MinLength,
Matches,
IsNumber,
Min,
Max,
ValidateNested,
IsArray,
ArrayMaxSize,
} from 'class-validator';
import { Type } from 'class-transformer';
export class OrderItemDto {
@IsNotEmpty()
@IsString()
productId: string;
@IsNumber()
@Min(1)
quantity: number;
}
export class CreateOrderDto {
@IsNotEmpty()
@IsString()
@MaxLength(100)
@Matches(/^[a-zA-Z0-9\s\-]+$/, {
message: 'Name contains invalid characters',
})
name: string;
@IsNotEmpty()
@IsString()
@Matches(/^[A-Z0-9\-]+$/, {
message: 'Code must be uppercase alphanumeric with hyphens',
})
@MaxLength(50)
code: string;
@IsOptional()
@IsEmail()
customerEmail?: string;
@IsNumber()
@Min(0)
@Max(1000000)
amount: number;
@IsOptional()
@IsArray()
@ArrayMaxSize(100)
@ValidateNested({ each: true })
@Type(() => OrderItemDto)
items?: OrderItemDto[];
}
Sanitize String Input
Prevent XSS and injection attacks by sanitizing string input.
import { Transform } from 'class-transformer';
import * as sanitizeHtml from 'sanitize-html';
export class CommentDto {
@IsString()
@MaxLength(1000)
@Transform(({ value }) => sanitizeHtml(value, {
allowedTags: [], // Strip all HTML
allowedAttributes: {},
}))
content: string;
@IsString()
@MaxLength(500)
@Transform(({ value }) => sanitizeHtml(value, {
allowedTags: ['b', 'i', 'em', 'strong'], // Allow basic formatting
allowedAttributes: {},
}))
description: string;
}
Code Injection Prevention
Never use dynamic code execution constructs or build shell commands from user-controlled strings. These patterns are flagged as AP022 and AP023 by the MCP anti-pattern checker.
Avoid eval() and new Function():
// Forbidden — executes arbitrary code from user input
const result = eval(userInput);
const fn = new Function('return ' + userInput)();
// Safe — use a fixed dispatch table instead
const OPERATIONS: Record<string, (a: number, b: number) => number> = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
};
const op = OPERATIONS[userInput]; // Look up only — never eval
if (!op) throw new BadRequestException('Unknown operation');
const result = op(1, 2);
Avoid shell commands built from string concatenation:
import { execSync } from 'child_process';
// Forbidden — shell injection risk when userInput contains special characters
const output = execSync(`ls ${userInput}`);
// Safe — use AWS SDK or fixed commands; never interpolate user input into shell strings
import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3';
const client = new S3Client({});
await client.send(new ListObjectsV2Command({ Bucket: 'my-bucket', Prefix: sanitizedPrefix }));
Validate File Uploads
Restrict file types, sizes, and scan for malware.
import { BadRequestException } from '@nestjs/common';
import FileType from 'file-type'; // npm install file-type
// Validate file type and size
const ALLOWED_MIME_TYPES = [
'image/jpeg',
'image/png',
'application/pdf',
'text/csv',
];
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
async function validateUpload(file: Express.Multer.File): Promise<void> {
// Check MIME type
if (!ALLOWED_MIME_TYPES.includes(file.mimetype)) {
throw new BadRequestException('File type not allowed');
}
// Check file size
if (file.size > MAX_FILE_SIZE) {
throw new BadRequestException('File too large');
}
// Verify file signature (magic bytes)
const fileType = await FileType.fromBuffer(file.buffer);
if (!fileType || !ALLOWED_MIME_TYPES.includes(fileType.mime)) {
throw new BadRequestException('Invalid file content');
}
}
Authentication
Configure Cognito Securely
Use strong password policies and MFA.
// CDK configuration for Cognito User Pool
const userPool = new cognito.UserPool(this, 'UserPool', {
selfSignUpEnabled: false, // Disable self-registration if not needed
signInAliases: {
email: true,
username: false,
},
passwordPolicy: {
minLength: 12,
requireLowercase: true,
requireUppercase: true,
requireDigits: true,
requireSymbols: true,
tempPasswordValidity: Duration.days(7),
},
mfa: cognito.Mfa.REQUIRED,
mfaSecondFactor: {
sms: true,
otp: true,
},
accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
advancedSecurityMode: cognito.AdvancedSecurityMode.ENFORCED,
});
The default implementation uses minLength: 6 for development convenience. For production environments, we strongly recommend using at least 12 characters as shown above, along with MFA enabled.
Validate JWT Tokens
Always validate JWT tokens on the server side.
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { CognitoJwtVerifier } from 'aws-jwt-verify';
@Injectable()
export class JwtAuthGuard implements CanActivate {
private verifier: CognitoJwtVerifier;
constructor() {
this.verifier = CognitoJwtVerifier.create({
userPoolId: process.env.COGNITO_USER_POOL_ID,
tokenUse: 'access',
clientId: process.env.COGNITO_USER_POOL_CLIENT_ID,
});
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractToken(request);
if (!token) {
throw new UnauthorizedException('Missing token');
}
try {
const payload = await this.verifier.verify(token);
request.user = payload;
return true;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
private extractToken(request: any): string | null {
const authHeader = request.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return null;
}
return authHeader.substring(7);
}
}
Token Refresh Strategy
Implement secure token refresh without storing refresh tokens on client.
// Frontend token refresh
async function refreshTokenIfNeeded(): Promise<string> {
try {
const session = await Auth.currentSession();
return session.getAccessToken().getJwtToken();
} catch (error) {
if (error.name === 'NotAuthorizedException') {
// Redirect to login
await Auth.signOut();
throw new Error('Session expired');
}
throw error;
}
}