LESSON 30分

ストーリー

高橋アーキテクト
脅威を分析して要件を定義した。次は、その要件を実現するための”設計パターン”だ

高橋アーキテクトがコードエディタを開きました。

高橋アーキテクト
デザインパターンと同じように、セキュリティにも”繰り返し使える設計の型”がある。これを知っていれば、毎回ゼロから考えなくて済む

パターン1: ゲートキーパー

全てのリクエストを単一のゲートで検証するパターンです。API Gatewayやミドルウェアがこの役割を担います。

// ゲートキーパーパターン:認証・認可ミドルウェア
type Middleware = (req: Request, res: Response, next: NextFunction) => void;

const securityGateway: Middleware[] = [
  // 1. レートリミット
  rateLimiter({ windowMs: 60000, max: 100 }),

  // 2. リクエストサイズ制限
  express.json({ limit: "10kb" }),

  // 3. セキュリティヘッダー
  helmet(),

  // 4. CORS
  cors({ origin: allowedOrigins }),

  // 5. 認証
  authenticate,

  // 6. リクエストログ
  requestLogger,
];

app.use("/api", ...securityGateway);

パターン2: 入力バリデーションパイプライン

全ての入力を多段階で検証するパターンです。

import { z } from "zod";

// バリデーションスキーマの定義
const createUserSchema = z.object({
  name: z.string()
    .min(1, "名前は必須です")
    .max(100, "名前は100文字以内です")
    .regex(/^[a-zA-Z\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\s]+$/,
      "使用できない文字が含まれています"),
  email: z.string()
    .email("有効なメールアドレスを入力してください")
    .max(254),
  age: z.number()
    .int("年齢は整数で入力してください")
    .min(0).max(150),
});

// バリデーションミドルウェア
const validate = <T>(schema: z.ZodSchema<T>) => {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({
        error: "Validation failed",
        details: result.error.issues.map(i => ({
          field: i.path.join("."),
          message: i.message,
        })),
      });
    }
    req.validatedBody = result.data;
    next();
  };
};

app.post("/api/users", validate(createUserSchema), createUserHandler);

パターン3: 安全なエラーハンドリング

内部情報を漏洩させずにエラーを返すパターンです。

// カスタムエラークラス
class AppError extends Error {
  constructor(
    public statusCode: number,
    public userMessage: string,
    public internalMessage: string, // ログにのみ記録
  ) {
    super(userMessage);
  }
}

// エラーハンドリングミドルウェア
const errorHandler = (
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction,
) => {
  // 内部情報をログに記録
  logger.error({
    error: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method,
    userId: req.user?.id,
  });

  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      error: err.userMessage, // ユーザーには安全なメッセージのみ
    });
  }

  // 予期しないエラーは汎用メッセージ
  res.status(500).json({
    error: "Internal server error",
    // スタックトレースやDB情報は絶対に返さない
  });
};

パターン4: トークンバケット(レートリミット)

リソースの過剰使用を防ぐパターンです。

interface RateLimitConfig {
  maxTokens: number;      // バケットの容量
  refillRate: number;     // 1秒あたりの補充数
  refillInterval: number; // 補充間隔(ms)
}

class TokenBucket {
  private tokens: number;
  private lastRefill: number;

  constructor(private config: RateLimitConfig) {
    this.tokens = config.maxTokens;
    this.lastRefill = Date.now();
  }

  tryConsume(): boolean {
    this.refill();
    if (this.tokens > 0) {
      this.tokens--;
      return true; // リクエスト許可
    }
    return false; // レート制限超過
  }

  private refill(): void {
    const now = Date.now();
    const elapsed = now - this.lastRefill;
    const tokensToAdd = Math.floor(
      elapsed / this.config.refillInterval * this.config.refillRate
    );
    this.tokens = Math.min(this.config.maxTokens, this.tokens + tokensToAdd);
    this.lastRefill = now;
  }
}

パターン5: Immutable Audit Log(不変監査ログ)

改ざん不可能な形で操作履歴を記録するパターンです。

interface AuditEntry {
  id: string;           // UUID
  timestamp: Date;
  actor: {
    userId: string;
    role: string;
    ipAddress: string;
  };
  action: string;       // "user.create", "order.delete" など
  resource: {
    type: string;
    id: string;
  };
  details: Record<string, unknown>;
  previousHash: string; // 前のエントリのハッシュ(チェーン)
  hash: string;         // このエントリのハッシュ
}

const createAuditEntry = async (
  entry: Omit<AuditEntry, "id" | "hash" | "previousHash">,
): Promise<AuditEntry> => {
  const previousEntry = await auditStore.getLatest();
  const previousHash = previousEntry?.hash ?? "GENESIS";

  const auditEntry: AuditEntry = {
    ...entry,
    id: crypto.randomUUID(),
    previousHash,
    hash: "", // 計算後に設定
  };

  // ハッシュチェーンで改ざんを検知可能に
  auditEntry.hash = crypto
    .createHash("sha256")
    .update(JSON.stringify({ ...auditEntry, hash: undefined }))
    .digest("hex");

  // 追記のみ可能なストレージに保存(DELETE不可)
  await auditStore.append(auditEntry);
  return auditEntry;
};

パターンの選択ガイド

脅威推奨パターン
不正アクセスゲートキーパー
インジェクション入力バリデーションパイプライン
情報漏洩安全なエラーハンドリング
DoS攻撃トークンバケット
否認Immutable Audit Log

まとめ

ポイント内容
ゲートキーパー単一ゲートで全リクエストを検証
バリデーションZodなどで型安全に入力を検証
エラーハンドリング内部情報を漏洩させない
監査ログハッシュチェーンで改ざん検知

チェックリスト

  • ゲートキーパーパターンの実装方法を理解した
  • 入力バリデーションパイプラインの設計を把握した
  • 安全なエラーハンドリングの重要性を理解した
  • 不変監査ログの仕組みを理解した

次のステップへ

次は演習です。ここまで学んだ脅威モデリングの知識を使って、実際にシステムの脅威分析を行いましょう。


推定読了時間: 30分