セキュリティベスト プラクティス
このガイドでは、入力バリデーション、認証、認可、データ保護など、MBC CQRS Serverlessで安全なアプリケーションを構築するためのセキュリティベストプラクティスについて説明します。
入力バリデーション
すべての入力をバリデーション
class-validatorデコレーターを使用して、API境界で常に入力をバリデーションします。
import {
IsNotEmpty,
IsString,
IsEmail,
IsOptional,
MaxLength,
MinLength,
Matches,
IsNumber,
Min,
Max,
ValidateNested,
IsArray,
ArrayMaxSize,
} from 'class-validator';
import { Type } from 'class-transformer';
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[];
}
文字列入力のサニタイズ
文字列入力をサニタイズしてXSSやインジェクション攻撃を防止します。
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;
}
ファイルアップロードのバリデーション
ファイルタイプ、サイズを制限し、マルウェアをスキャンします。
// 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');
}
}
認証
Cognitoのセキュア設定
強力なパスワードポリシーと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,
});
デフォルト実装に関する注記
デ フォルト実装では開発の利便性のため minLength: 6 を使用しています。本番環境では、上記のように最低12文字以上を設定し、MFAを有効にすることを強く推奨します。
JWTトークンのバリデーション
サーバー側で常にJWTトークンをバリデーションします。
import { Injectable, CanActivate, ExecutionContext } 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_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);
}
}
トークンリフレッシュ戦略
クライアントにリフレッシュトークンを保存せずに、安全なトークンリフレッシュを実装します。
// 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;
}
}
認可
ロールベースアクセス制御の実装
ロールベースの認可にCognitoグループを使用します。
import { SetMetadata, CanActivate, ExecutionContext } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
const userGroups = user['cognito:groups'] || [];
return requiredRoles.some(role => userGroups.includes(role));
}
}
// Usage in controller
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('users')
@Roles('admin', 'super-admin')
findAllUsers() {
// Only admin or super-admin can access
}
}