セキュリティベストプラクティス
このガイドでは、入力バリデーション、認証、認可、データ保護など、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 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[];
}
文字列入力のサニタイズ
文字列入力をサニタイズしてXSSやインジェクション攻撃を防止します。
import { Transform } from 'class-transformer';
import * as sanitizeHtml from 'sanitize-html';
export class CommentDto {
@IsString()
@MaxLength(1000)
@Transform(({ value }) => sanitizeHtml(value, {
allowedTags: [], // すべての HTML を除去
allowedAttributes: {},
}))
content: string;
@IsString()
@MaxLength(500)
@Transform(({ value }) => sanitizeHtml(value, {
allowedTags: ['b', 'i', 'em', 'strong'], // 基本的な書式設定のみ許可
allowedAttributes: {},
}))
description: string;
}
コードインジェクション防止
動的なコード実行構造を使用したり、ユーザーが制御する文字列からシェルコマンドを構築しないでください。これらのパターンはMCPアンチパターンチェッカーによりAP022およびAP023としてフラグされます。
eval()とnew Function()を避ける:
// 禁止 — ユーザー入力から任意のコードを実行する
const result = eval(userInput);
const fn = new Function('return ' + userInput)();
// 安全 — 代わりに固定のディスパッチテーブルを使用する
const OPERATIONS: Record<string, (a: number, b: number) => number> = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
};
const op = OPERATIONS[userInput]; // ルックアップのみ — evalは絶対に使用しない
if (!op) throw new BadRequestException('Unknown operation');
const result = op(1, 2);
文字列結合によるシェルコマンドを避ける:
import { execSync } from 'child_process';
// 禁止 — userInputに特殊文字が含まれる場合のシェルインジェクションリスク
const output = execSync(`ls ${userInput}`);
// 安全 — AWS SDKまたは固定コマンドを使用する。シェル文字列にユーザー入力を補間しない
import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3';
const client = new S3Client({});
await client.send(new ListObjectsV2Command({ Bucket: 'my-bucket', Prefix: sanitizedPrefix }));
ファイルアップロードのバリデーション
ファイルタイプ、サイズを制限し、マルウェアをスキャンします。
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 (MIMEタイプを確認)
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 (Cognito User Pool用CDK設定)
const userPool = new cognito.UserPool(this, 'UserPool', {
selfSignUpEnabled: false, // 不要な場合はセルフ登録を無効化
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, 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);
}
}