EXERCISE 80分

ストーリー

佐藤CTO
Step 4で脆弱性の種類、診断手法、APIセキュリティ、修正と報告を学んだ

佐藤CTOが脆弱性診断レポートのテンプレートを手渡しました。

佐藤CTO
いよいよ実践だ。ECプラットフォーム「ShopNow」の脆弱性診断を実施してくれ。OWASP ZAPでのスキャン設計から、SQLインジェクションの評価、APIエンドポイントの監査、そして修正計画の策定まで — フルサイクルを回そう

対象プロジェクト:ECプラットフォーム「ShopNow」

技術スタック

コンポーネント技術
バックエンドTypeScript (Node.js / Express)
フロントエンドNext.js
データベースPostgreSQL + Redis
API仕様OpenAPI 3.0
認証JWT + OAuth2.0 (Auth0)
インフラAWS (ECS Fargate + ALB + RDS)

診断対象エンドポイント

エンドポイントメソッド認証機能
/api/v1/productsGET不要商品一覧・検索
/api/v1/products/
GET不要商品詳細
/api/v1/cartPOST/PUT/DELETE必要カート操作
/api/v1/ordersPOST必要注文作成
/api/v1/orders/
GET必要注文詳細
/api/v1/users/profileGET/PUT必要プロフィール管理
/api/v1/admin/usersGET管理者ユーザー管理
/api/v1/admin/reportsGET管理者レポート生成

Mission 1:OWASP ZAPスキャン設計(20分)

ステージング環境のShopNowに対するOWASP ZAPの自動スキャン設計を行ってください。

要件

  1. スキャンポリシーの設計(対象スキャンルール、閾値設定)
  2. 認証設定(JWT Bearer トークンを使用した認証済みスキャン)
  3. コンテキストの定義(対象URL、除外URL、セッション管理)
  4. 自動化スクリプト(ZAP APIを利用したスキャン自動化)
解答例
# zap-scan-config.yaml
# OWASP ZAPスキャンポリシー設計

env:
  contexts:
    - name: "ShopNow EC Platform"
      urls:
        - "https://staging.shopnow.example.com"
      includePaths:
        - "https://staging.shopnow.example.com/api/v1/.*"
        - "https://staging.shopnow.example.com/.*"
      excludePaths:
        - "https://staging.shopnow.example.com/api/v1/health"
        - "https://staging.shopnow.example.com/api/v1/metrics"
        - "https://staging.shopnow.example.com/logout"
      authentication:
        method: "json"
        parameters:
          loginPageUrl: "https://staging.shopnow.example.com/api/v1/auth/login"
          loginRequestUrl: "https://staging.shopnow.example.com/api/v1/auth/login"
          loginRequestBody: '{"email":"{%username%}","password":"{%password%}"}'
        verification:
          method: "response"
          loggedInRegex: "\\Qaccess_token\\E"
          loggedOutRegex: "\\QUnauthorized\\E"
      sessionManagement:
        method: "headers"
        parameters:
          - header: "Authorization"
            value: "Bearer {%token%}"
      users:
        - name: "test-user"
          credentials:
            username: "test@shopnow.example.com"
            password: "${ZAP_TEST_USER_PASSWORD}"
        - name: "admin-user"
          credentials:
            username: "admin@shopnow.example.com"
            password: "${ZAP_ADMIN_USER_PASSWORD}"

  parameters:
    failOnError: true
    failOnWarning: false
    progressToStdout: true

jobs:
  # Phase 1: パッシブスキャン(情報収集)
  - type: passiveScan-config
    parameters:
      maxAlertsPerRule: 10
      scanOnlyInScope: true
      maxBodySizeInBytesToScan: 10000

  # Phase 2: スパイダー(クローリング)
  - type: spider
    parameters:
      context: "ShopNow EC Platform"
      user: "test-user"
      maxDuration: 10       # 10分制限
      maxDepth: 5
      maxChildren: 10

  # Phase 3: OpenAPI仕様ベースのインポート
  - type: openapi
    parameters:
      apiUrl: "https://staging.shopnow.example.com/api/v1/openapi.json"
      context: "ShopNow EC Platform"

  # Phase 4: アクティブスキャン
  - type: activeScan
    parameters:
      context: "ShopNow EC Platform"
      user: "test-user"
      maxRuleDurationInMins: 5
      maxScanDurationInMins: 60
      policy: "shopnow-scan-policy"

  # Phase 5: レポート生成
  - type: report
    parameters:
      template: "traditional-html-plus"
      reportDir: "/zap/reports"
      reportFile: "shopnow-vuln-report"
    risks:
      - high
      - medium
      - low
      - info
// zap-automation.ts - ZAP APIを使ったスキャン自動化スクリプト
import axios from 'axios';

interface ZapScanConfig {
  zapBaseUrl: string;
  targetUrl: string;
  apiKey: string;
  openApiSpecUrl: string;
  authToken: string;
}

class ZapAutomatedScanner {
  private config: ZapScanConfig;
  private zapApi: ReturnType<typeof axios.create>;

  constructor(config: ZapScanConfig) {
    this.config = config;
    this.zapApi = axios.create({
      baseURL: config.zapBaseUrl,
      params: { apikey: config.apiKey },
    });
  }

  async runFullScan(): Promise<ScanResult> {
    console.log('[ZAP] Starting full vulnerability scan...');

    // Step 1: コンテキスト設定
    const contextId = await this.setupContext();

    // Step 2: 認証設定
    await this.configureAuthentication(contextId);

    // Step 3: OpenAPI仕様インポート
    await this.importOpenApiSpec();

    // Step 4: スパイダー実行
    await this.runSpider(contextId);

    // Step 5: パッシブスキャン完了待ち
    await this.waitForPassiveScan();

    // Step 6: アクティブスキャン実行
    await this.runActiveScan(contextId);

    // Step 7: 結果収集
    const alerts = await this.collectAlerts();

    return {
      totalAlerts: alerts.length,
      highRisk: alerts.filter(a => a.risk === 'High').length,
      mediumRisk: alerts.filter(a => a.risk === 'Medium').length,
      lowRisk: alerts.filter(a => a.risk === 'Low').length,
      informational: alerts.filter(a => a.risk === 'Informational').length,
      alerts,
    };
  }

  private async setupContext(): Promise<string> {
    const res = await this.zapApi.get('/JSON/context/action/newContext/', {
      params: { contextName: 'ShopNow' },
    });
    const contextId = res.data.contextId;

    // スコープ設定
    await this.zapApi.get('/JSON/context/action/includeInContext/', {
      params: {
        contextName: 'ShopNow',
        regex: `${this.config.targetUrl}/.*`,
      },
    });

    // 除外パス
    const excludePaths = ['/api/v1/health', '/api/v1/metrics', '/logout'];
    for (const path of excludePaths) {
      await this.zapApi.get('/JSON/context/action/excludeFromContext/', {
        params: {
          contextName: 'ShopNow',
          regex: `${this.config.targetUrl}${path}`,
        },
      });
    }

    return contextId;
  }

  private async configureAuthentication(contextId: string): Promise<void> {
    // JSON Based Authentication
    await this.zapApi.get('/JSON/authentication/action/setAuthenticationMethod/', {
      params: {
        contextId,
        authMethodName: 'jsonBasedAuthentication',
        authMethodConfigParams: [
          `loginUrl=${this.config.targetUrl}/api/v1/auth/login`,
          'loginRequestData={"email":"{%username%}","password":"{%password%}"}',
        ].join('&'),
      },
    });

    // セッション管理: HTTPヘッダーベース
    await this.zapApi.get('/JSON/sessionManagement/action/setSessionManagementMethod/', {
      params: {
        contextId,
        methodName: 'httpAuthSessionManagement',
      },
    });
  }

  private async importOpenApiSpec(): Promise<void> {
    await this.zapApi.get('/JSON/openapi/action/importUrl/', {
      params: {
        url: this.config.openApiSpecUrl,
        hostOverride: new URL(this.config.targetUrl).host,
      },
    });
  }

  private async runSpider(contextId: string): Promise<void> {
    const res = await this.zapApi.get('/JSON/spider/action/scan/', {
      params: {
        contextName: 'ShopNow',
        url: this.config.targetUrl,
        maxChildren: 10,
        recurse: true,
        subtreeOnly: true,
      },
    });
    const scanId = res.data.scan;

    // スパイダー完了待ち
    await this.waitForScanCompletion(
      '/JSON/spider/view/status/',
      scanId,
      'Spider',
    );
  }

  private async waitForPassiveScan(): Promise<void> {
    let recordsRemaining = 1;
    while (recordsRemaining > 0) {
      const res = await this.zapApi.get('/JSON/pscan/view/recordsToScan/');
      recordsRemaining = parseInt(res.data.recordsToScan, 10);
      if (recordsRemaining > 0) {
        console.log(`[ZAP] Passive scan: ${recordsRemaining} records remaining`);
        await this.sleep(2000);
      }
    }
  }

  private async runActiveScan(contextId: string): Promise<void> {
    const res = await this.zapApi.get('/JSON/ascan/action/scan/', {
      params: {
        url: this.config.targetUrl,
        contextId,
        recurse: true,
        scanPolicyName: 'Default Policy',
      },
    });
    const scanId = res.data.scan;

    await this.waitForScanCompletion(
      '/JSON/ascan/view/status/',
      scanId,
      'Active Scan',
    );
  }

  private async collectAlerts(): Promise<ZapAlert[]> {
    const res = await this.zapApi.get('/JSON/alert/view/alerts/', {
      params: { baseurl: this.config.targetUrl, start: 0, count: 500 },
    });
    return res.data.alerts;
  }

  private async waitForScanCompletion(
    statusEndpoint: string,
    scanId: string,
    scanName: string,
  ): Promise<void> {
    let progress = 0;
    while (progress < 100) {
      const res = await this.zapApi.get(statusEndpoint, {
        params: { scanId },
      });
      progress = parseInt(res.data.status, 10);
      console.log(`[ZAP] ${scanName}: ${progress}%`);
      if (progress < 100) await this.sleep(5000);
    }
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

interface ScanResult {
  totalAlerts: number;
  highRisk: number;
  mediumRisk: number;
  lowRisk: number;
  informational: number;
  alerts: ZapAlert[];
}

interface ZapAlert {
  risk: 'High' | 'Medium' | 'Low' | 'Informational';
  confidence: string;
  name: string;
  description: string;
  url: string;
  param: string;
  evidence: string;
  cweid: string;
  wascid: string;
  solution: string;
}

Mission 2:SQLインジェクション評価(20分)

ShopNowの商品検索エンドポイントにおけるSQLインジェクションの脆弱性を評価し、安全な実装を設計してください。

要件

  1. 脆弱なコード例の分析と攻撃パターンの列挙
  2. 安全な実装(パラメータ化クエリ、ORM活用)
  3. 多層防御(WAFルール、入力バリデーション、最小権限DB設定)
  4. テストケース(自動テストでSQLインジェクション耐性を検証)

脆弱なコード例

// !! 脆弱なコード - 絶対に本番で使用しないでください !!
app.get('/api/v1/products', async (req, res) => {
  const { keyword, category, minPrice, maxPrice, sortBy } = req.query;

  let query = `SELECT * FROM products WHERE 1=1`;
  if (keyword) query += ` AND name LIKE '%${keyword}%'`;
  if (category) query += ` AND category = '${category}'`;
  if (minPrice) query += ` AND price >= ${minPrice}`;
  if (maxPrice) query += ` AND price <= ${maxPrice}`;
  if (sortBy) query += ` ORDER BY ${sortBy}`;

  const result = await db.query(query);
  res.json(result.rows);
});
解答例

攻撃パターン分析

// 攻撃パターン1: UNION-based SQLi(データ抽出)
// keyword = "' UNION SELECT username, password, email, null, null, null FROM users --"
// 生成されるSQL:
// SELECT * FROM products WHERE 1=1 AND name LIKE '%' UNION SELECT username, password, email, null, null, null FROM users --%'

// 攻撃パターン2: Boolean-based Blind SQLi(情報推測)
// keyword = "' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin')='a' --"

// 攻撃パターン3: Time-based Blind SQLi(応答時間による推測)
// keyword = "' AND (SELECT CASE WHEN (1=1) THEN pg_sleep(5) ELSE pg_sleep(0) END) --"

// 攻撃パターン4: Error-based SQLi(エラーメッセージから情報取得)
// category = "' AND 1=CAST((SELECT version()) AS int) --"

// 攻撃パターン5: ORDER BY injection
// sortBy = "name; DROP TABLE products; --"

// 攻撃パターン6: Stacked queries(複数クエリ実行)
// minPrice = "0; UPDATE users SET role='admin' WHERE email='attacker@evil.com'; --"

安全な実装

import { z } from 'zod';
import { PrismaClient, Prisma } from '@prisma/client';

const prisma = new PrismaClient();

// 入力バリデーションスキーマ
const ProductSearchSchema = z.object({
  keyword: z.string().max(100).optional(),
  category: z.enum([
    'electronics', 'clothing', 'books', 'food', 'toys',
  ]).optional(),
  minPrice: z.coerce.number().min(0).max(10000000).optional(),
  maxPrice: z.coerce.number().min(0).max(10000000).optional(),
  sortBy: z.enum(['name', 'price', 'createdAt', 'rating']).optional(),
  sortOrder: z.enum(['asc', 'desc']).optional().default('asc'),
  page: z.coerce.number().int().min(1).optional().default(1),
  limit: z.coerce.number().int().min(1).max(100).optional().default(20),
});

// 安全な実装(Prisma ORM使用)
app.get('/api/v1/products', async (req, res) => {
  // Step 1: 入力バリデーション
  const parseResult = ProductSearchSchema.safeParse(req.query);
  if (!parseResult.success) {
    return res.status(400).json({
      error: 'Invalid parameters',
      details: parseResult.error.issues,
    });
  }

  const { keyword, category, minPrice, maxPrice, sortBy, sortOrder, page, limit } = parseResult.data;

  // Step 2: Prismaの型安全なクエリ構築
  const where: Prisma.ProductWhereInput = {
    ...(keyword && {
      name: {
        contains: keyword,
        mode: 'insensitive' as Prisma.QueryMode,
      },
    }),
    ...(category && { category }),
    ...(minPrice !== undefined || maxPrice !== undefined) && {
      price: {
        ...(minPrice !== undefined && { gte: minPrice }),
        ...(maxPrice !== undefined && { lte: maxPrice }),
      },
    },
    isActive: true,  // 論理削除されていない商品のみ
  };

  const orderBy: Prisma.ProductOrderByWithRelationInput = sortBy
    ? { [sortBy]: sortOrder }
    : { createdAt: 'desc' };

  // Step 3: ページネーション付きクエリ実行
  const [products, total] = await Promise.all([
    prisma.product.findMany({
      where,
      orderBy,
      skip: (page - 1) * limit,
      take: limit,
      select: {
        id: true,
        name: true,
        description: true,
        price: true,
        category: true,
        imageUrl: true,
        rating: true,
        createdAt: true,
      },
    }),
    prisma.product.count({ where }),
  ]);

  res.json({
    data: products,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
    },
  });
});

多層防御

// Layer 1: WAFルール(AWS WAF)
// SQLインジェクション検出ルール
const wafSqlInjectionRule = {
  Name: 'SQLInjectionProtection',
  Priority: 1,
  Statement: {
    OrStatement: {
      Statements: [
        {
          SqliMatchStatement: {
            FieldToMatch: { QueryString: {} },
            TextTransformations: [
              { Priority: 0, Type: 'URL_DECODE' },
              { Priority: 1, Type: 'HTML_ENTITY_DECODE' },
            ],
          },
        },
        {
          SqliMatchStatement: {
            FieldToMatch: { Body: {} },
            TextTransformations: [
              { Priority: 0, Type: 'URL_DECODE' },
              { Priority: 1, Type: 'HTML_ENTITY_DECODE' },
            ],
          },
        },
      ],
    },
  },
  Action: { Block: {} },
};

// Layer 2: アプリケーションレベルのサニタイゼーション
function sanitizeSearchInput(input: string): string {
  // 制御文字の除去
  const sanitized = input.replace(/[\x00-\x1F\x7F]/g, '');
  // 最大長制限
  return sanitized.substring(0, 100);
}

// Layer 3: データベースユーザーの最小権限
// PostgreSQLでのロール設定
/*
-- 読み取り専用ロール(商品検索用)
CREATE ROLE shopnow_readonly;
GRANT CONNECT ON DATABASE shopnow TO shopnow_readonly;
GRANT USAGE ON SCHEMA public TO shopnow_readonly;
GRANT SELECT ON products, categories TO shopnow_readonly;

-- 書き込みロール(注文処理用)
CREATE ROLE shopnow_writer;
GRANT CONNECT ON DATABASE shopnow TO shopnow_writer;
GRANT USAGE ON SCHEMA public TO shopnow_writer;
GRANT SELECT, INSERT, UPDATE ON orders, order_items, cart TO shopnow_writer;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO shopnow_writer;

-- 管理者ロール(マイグレーション用のみ)
CREATE ROLE shopnow_admin;
GRANT ALL PRIVILEGES ON DATABASE shopnow TO shopnow_admin;
*/

// Layer 4: クエリログ監視
const queryLogger = {
  async logSuspiciousQuery(query: string, params: unknown[], source: string): Promise<void> {
    const suspiciousPatterns = [
      /UNION\s+SELECT/i,
      /;\s*DROP/i,
      /;\s*DELETE/i,
      /--\s*$/,
      /\/\*.*\*\//,
      /SLEEP\s*\(/i,
      /BENCHMARK\s*\(/i,
      /pg_sleep/i,
      /WAITFOR\s+DELAY/i,
    ];

    for (const pattern of suspiciousPatterns) {
      if (pattern.test(query)) {
        console.error(`[SECURITY] Suspicious query pattern detected from ${source}: ${query}`);
        // セキュリティアラートを送信
        await sendSecurityAlert({
          type: 'SQL_INJECTION_ATTEMPT',
          severity: 'HIGH',
          source,
          query: query.substring(0, 500),
          timestamp: new Date().toISOString(),
        });
        break;
      }
    }
  },
};

SQLインジェクション耐性テスト

// __tests__/security/sql-injection.test.ts
import request from 'supertest';
import { app } from '../../src/app';

describe('SQL Injection Resistance Tests', () => {
  const sqlInjectionPayloads = [
    // Classic payloads
    "' OR '1'='1",
    "' OR '1'='1' --",
    "'; DROP TABLE products; --",
    "' UNION SELECT null, null, null --",

    // Blind SQLi payloads
    "' AND 1=1 --",
    "' AND 1=2 --",
    "' AND SLEEP(5) --",

    // Encoding bypass
    "%27%20OR%20%271%27%3D%271",
    "&#39; OR &#39;1&#39;=&#39;1",

    // PostgreSQL specific
    "'; SELECT pg_sleep(5); --",
    "' AND (SELECT version())::int > 0 --",
  ];

  describe('GET /api/v1/products - keyword parameter', () => {
    test.each(sqlInjectionPayloads)(
      'should safely handle payload: %s',
      async (payload) => {
        const response = await request(app)
          .get('/api/v1/products')
          .query({ keyword: payload });

        // SQLエラーが発生しないこと
        expect(response.status).not.toBe(500);

        // レスポンスにDB情報が漏洩しないこと
        const body = JSON.stringify(response.body);
        expect(body).not.toMatch(/postgresql/i);
        expect(body).not.toMatch(/syntax error/i);
        expect(body).not.toMatch(/pg_catalog/i);
        expect(body).not.toMatch(/information_schema/i);
      },
    );
  });

  describe('GET /api/v1/products - sortBy parameter', () => {
    test('should reject invalid sortBy values', async () => {
      const response = await request(app)
        .get('/api/v1/products')
        .query({ sortBy: 'name; DROP TABLE products' });

      expect(response.status).toBe(400);
    });

    test('should accept only whitelisted sortBy values', async () => {
      const validSortFields = ['name', 'price', 'createdAt', 'rating'];
      for (const field of validSortFields) {
        const response = await request(app)
          .get('/api/v1/products')
          .query({ sortBy: field });

        expect(response.status).toBe(200);
      }
    });
  });

  describe('Response time consistency (Blind SQLi detection)', () => {
    test('should not show timing differences for boolean-based payloads', async () => {
      const start1 = Date.now();
      await request(app)
        .get('/api/v1/products')
        .query({ keyword: "' AND 1=1 --" });
      const duration1 = Date.now() - start1;

      const start2 = Date.now();
      await request(app)
        .get('/api/v1/products')
        .query({ keyword: "' AND 1=2 --" });
      const duration2 = Date.now() - start2;

      // 応答時間の差が500ms以内であること(タイミング攻撃の防止)
      expect(Math.abs(duration1 - duration2)).toBeLessThan(500);
    });
  });
});

Mission 3:APIエンドポイントセキュリティ監査(20分)

ShopNowのREST APIエンドポイントに対するセキュリティ監査チェックリストを作成し、主要な脆弱性を評価してください。

要件

  1. APIセキュリティ監査チェックリスト(OWASP API Security Top 10ベース)
  2. 認証・認可の検証シナリオ(BOLA、BFLA、水平権限昇格)
  3. レート制限・不正利用防止の設計
  4. 監査レポートテンプレート
解答例

APIセキュリティ監査チェックリスト

// api-security-audit.ts

interface AuditCheckItem {
  id: string;
  category: string;
  owaspApiRef: string;
  checkDescription: string;
  severity: 'Critical' | 'High' | 'Medium' | 'Low';
  status: 'Pass' | 'Fail' | 'N/A' | 'Pending';
  evidence: string;
  remediation: string;
}

const apiSecurityAuditChecklist: AuditCheckItem[] = [
  // API1:2023 - Broken Object Level Authorization (BOLA)
  {
    id: 'API1-01',
    category: 'Object Level Authorization',
    owaspApiRef: 'API1:2023',
    checkDescription: '他ユーザーの注文詳細にアクセスできないことを確認',
    severity: 'Critical',
    status: 'Pending',
    evidence: 'GET /api/v1/orders/{other_user_order_id} with valid JWT',
    remediation: 'オブジェクトレベルの認可チェックを全エンドポイントに実装',
  },
  {
    id: 'API1-02',
    category: 'Object Level Authorization',
    owaspApiRef: 'API1:2023',
    checkDescription: '他ユーザーのプロフィールを変更できないことを確認',
    severity: 'Critical',
    status: 'Pending',
    evidence: 'PUT /api/v1/users/profile with modified userId in body',
    remediation: 'JWTのsubjectからユーザーIDを取得し、リクエストボディのIDを無視',
  },

  // API2:2023 - Broken Authentication
  {
    id: 'API2-01',
    category: 'Authentication',
    owaspApiRef: 'API2:2023',
    checkDescription: 'JWTの署名検証が正しく行われていることを確認',
    severity: 'Critical',
    status: 'Pending',
    evidence: '改ざんしたJWTでのアクセス試行',
    remediation: 'RS256アルゴリズムの使用、alg=noneの拒否',
  },
  {
    id: 'API2-02',
    category: 'Authentication',
    owaspApiRef: 'API2:2023',
    checkDescription: '期限切れトークンが拒否されることを確認',
    severity: 'High',
    status: 'Pending',
    evidence: '期限切れJWTでのアクセス試行',
    remediation: 'exp claimの検証、適切なトークン有効期限の設定',
  },

  // API3:2023 - Broken Object Property Level Authorization
  {
    id: 'API3-01',
    category: 'Property Level Authorization',
    owaspApiRef: 'API3:2023',
    checkDescription: '一般ユーザーがroleフィールドを変更できないことを確認',
    severity: 'High',
    status: 'Pending',
    evidence: 'PUT /api/v1/users/profile with {"role": "admin"} in body',
    remediation: 'Mass Assignment防止: 許可されたフィールドのみ受け付けるスキーマ検証',
  },

  // API4:2023 - Unrestricted Resource Consumption
  {
    id: 'API4-01',
    category: 'Rate Limiting',
    owaspApiRef: 'API4:2023',
    checkDescription: 'レート制限が適切に設定されていることを確認',
    severity: 'Medium',
    status: 'Pending',
    evidence: '高頻度リクエストの送信テスト',
    remediation: 'エンドポイントごとのレート制限を実装',
  },

  // API5:2023 - Broken Function Level Authorization (BFLA)
  {
    id: 'API5-01',
    category: 'Function Level Authorization',
    owaspApiRef: 'API5:2023',
    checkDescription: '一般ユーザーが管理者APIにアクセスできないことを確認',
    severity: 'Critical',
    status: 'Pending',
    evidence: 'GET /api/v1/admin/users with regular user JWT',
    remediation: 'ロールベースアクセス制御(RBAC)の実装',
  },
];

// 認証・認可の検証シナリオ
interface AuthTestScenario {
  name: string;
  type: 'BOLA' | 'BFLA' | 'HorizontalEscalation' | 'VerticalEscalation';
  steps: string[];
  expectedResult: string;
}

const authTestScenarios: AuthTestScenario[] = [
  {
    name: 'BOLA: 他ユーザーの注文へのアクセス',
    type: 'BOLA',
    steps: [
      '1. ユーザーAとしてログインし、注文を作成(order_id取得)',
      '2. ユーザーBとしてログイン',
      '3. GET /api/v1/orders/{user_a_order_id} をユーザーBのトークンで実行',
    ],
    expectedResult: '403 Forbidden が返されること',
  },
  {
    name: 'BFLA: 一般ユーザーの管理者操作',
    type: 'BFLA',
    steps: [
      '1. 一般ユーザーとしてログイン',
      '2. GET /api/v1/admin/users を実行',
      '3. DELETE /api/v1/admin/users/{id} を実行',
    ],
    expectedResult: '403 Forbidden が返されること',
  },
  {
    name: '水平権限昇格: プロフィールの不正更新',
    type: 'HorizontalEscalation',
    steps: [
      '1. ユーザーAとしてログイン',
      '2. PUT /api/v1/users/profile に userId: "user_b_id" を含めて送信',
      '3. ユーザーBのプロフィールが変更されていないことを確認',
    ],
    expectedResult: 'JWTのsubjectのユーザーのみ更新されること',
  },
  {
    name: '垂直権限昇格: ロール変更の試行',
    type: 'VerticalEscalation',
    steps: [
      '1. 一般ユーザーとしてログイン',
      '2. PUT /api/v1/users/profile に {"role": "admin"} を含めて送信',
      '3. ユーザーのロールが変更されていないことを確認',
    ],
    expectedResult: 'roleフィールドは無視され、変更不可',
  },
];

レート制限設計

// rate-limiter.ts
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL!);

// エンドポイントごとのレート制限設定
const rateLimitConfig: Record<string, { windowMs: number; max: number }> = {
  // 認証エンドポイント(厳格)
  'POST /api/v1/auth/login':      { windowMs: 15 * 60 * 1000, max: 5 },
  'POST /api/v1/auth/register':   { windowMs: 60 * 60 * 1000, max: 3 },
  'POST /api/v1/auth/reset-password': { windowMs: 60 * 60 * 1000, max: 3 },

  // 一般エンドポイント(標準)
  'GET /api/v1/products':         { windowMs: 60 * 1000, max: 60 },
  'POST /api/v1/orders':          { windowMs: 60 * 1000, max: 10 },
  'PUT /api/v1/users/profile':    { windowMs: 60 * 1000, max: 10 },

  // 管理者エンドポイント(中程度)
  'GET /api/v1/admin/users':      { windowMs: 60 * 1000, max: 30 },
  'GET /api/v1/admin/reports':    { windowMs: 60 * 1000, max: 10 },
};

function createRateLimiter(windowMs: number, max: number) {
  return rateLimit({
    store: new RedisStore({ sendCommand: (...args: string[]) => redis.call(...args) }),
    windowMs,
    max,
    standardHeaders: true,
    legacyHeaders: false,
    keyGenerator: (req) => {
      // IP + ユーザーIDの組み合わせでキー生成
      const userId = req.user?.id || 'anonymous';
      return `${req.ip}:${userId}`;
    },
    handler: (req, res) => {
      res.status(429).json({
        error: 'Too Many Requests',
        retryAfter: Math.ceil(windowMs / 1000),
      });
    },
  });
}

監査レポートテンプレート

interface SecurityAuditReport {
  metadata: {
    projectName: string;
    auditDate: string;
    auditor: string;
    scope: string[];
    methodology: string;
    toolsUsed: string[];
  };
  executiveSummary: {
    overallRiskLevel: 'Critical' | 'High' | 'Medium' | 'Low';
    totalFindings: number;
    criticalFindings: number;
    highFindings: number;
    mediumFindings: number;
    lowFindings: number;
    topRisks: string[];
  };
  findings: AuditFinding[];
  recommendations: {
    immediate: string[];   // 24時間以内
    shortTerm: string[];   // 1週間以内
    longTerm: string[];    // 1ヶ月以内
  };
}

interface AuditFinding {
  id: string;
  title: string;
  severity: 'Critical' | 'High' | 'Medium' | 'Low';
  cvssScore: number;
  cvssVector: string;
  description: string;
  affectedEndpoints: string[];
  proofOfConcept: string;
  impact: string;
  remediation: string;
  references: string[];
}

// レポート出力例
const sampleReport: SecurityAuditReport = {
  metadata: {
    projectName: 'ShopNow EC Platform',
    auditDate: '2026-02-14',
    auditor: 'Security Team',
    scope: ['REST API v1', 'Authentication flow', 'Admin endpoints'],
    methodology: 'OWASP API Security Top 10 + Manual Testing',
    toolsUsed: ['OWASP ZAP', 'Burp Suite Pro', 'Custom scripts'],
  },
  executiveSummary: {
    overallRiskLevel: 'High',
    totalFindings: 12,
    criticalFindings: 2,
    highFindings: 3,
    mediumFindings: 4,
    lowFindings: 3,
    topRisks: [
      'BOLA: 注文APIでのオブジェクトレベル認可の欠如',
      'Mass Assignment: プロフィール更新でのroleフィールドの上書き可能性',
      'レート制限の不在: 認証エンドポイントでのブルートフォース攻撃リスク',
    ],
  },
  findings: [],
  recommendations: {
    immediate: [
      'BOLA脆弱性の修正(注文API、プロフィールAPI)',
      '認証エンドポイントへのレート制限の追加',
    ],
    shortTerm: [
      'Mass Assignment防止のためのスキーマバリデーション強化',
      'セキュリティヘッダーの追加(HSTS、CSP、X-Content-Type-Options)',
      'JWTトークンの有効期限短縮(15分→5分)',
    ],
    longTerm: [
      'APIゲートウェイでの集中的なレート制限と認可の実装',
      '定期的な自動ペネトレーションテストのCI/CD統合',
      'セキュリティログの集中管理とSIEM連携',
    ],
  },
};

Mission 4:修正計画の策定(20分)

Mission 1-3で発見された脆弱性に基づいて、優先順位付きの修正計画を策定してください。

要件

  1. 脆弱性の優先順位付け(CVSS + ビジネスインパクト)
  2. 修正ロードマップ(即時対応、短期対応、中期対応)
  3. 修正完了の検証方法(回帰テスト、再スキャン)
  4. 残留リスクの評価と受容判断
解答例
// remediation-plan.ts

interface RemediationPlan {
  planId: string;
  createdAt: string;
  approvedBy: string;
  vulnerabilities: PrioritizedVulnerability[];
  phases: RemediationPhase[];
  verificationPlan: VerificationPlan;
  residualRiskAssessment: ResidualRisk[];
}

interface PrioritizedVulnerability {
  vulnId: string;
  title: string;
  cvssScore: number;
  businessImpact: 'Critical' | 'High' | 'Medium' | 'Low';
  exploitability: 'Active' | 'PoC' | 'Theoretical';
  priorityScore: number;  // 計算済み
  phase: 'immediate' | 'short-term' | 'mid-term';
  assignedTeam: string;
  estimatedEffort: string;
}

const remediationPlan: RemediationPlan = {
  planId: 'RP-2026-003',
  createdAt: '2026-02-14',
  approvedBy: '佐藤CTO',
  vulnerabilities: [
    {
      vulnId: 'VULN-001',
      title: 'BOLA: 注文APIでのオブジェクトレベル認可の欠如',
      cvssScore: 8.6,
      businessImpact: 'Critical',
      exploitability: 'PoC',
      priorityScore: 95,
      phase: 'immediate',
      assignedTeam: 'Backend Team',
      estimatedEffort: '4時間',
    },
    {
      vulnId: 'VULN-002',
      title: 'SQLインジェクション: 商品検索の動的クエリ構築',
      cvssScore: 9.8,
      businessImpact: 'Critical',
      exploitability: 'Active',
      priorityScore: 100,
      phase: 'immediate',
      assignedTeam: 'Backend Team',
      estimatedEffort: '8時間',
    },
    {
      vulnId: 'VULN-003',
      title: 'Mass Assignment: プロフィール更新でのrole上書き',
      cvssScore: 7.5,
      businessImpact: 'High',
      exploitability: 'PoC',
      priorityScore: 80,
      phase: 'immediate',
      assignedTeam: 'Backend Team',
      estimatedEffort: '2時間',
    },
    {
      vulnId: 'VULN-004',
      title: 'レート制限の不在: 認証エンドポイント',
      cvssScore: 5.3,
      businessImpact: 'Medium',
      exploitability: 'Active',
      priorityScore: 65,
      phase: 'short-term',
      assignedTeam: 'Platform Team',
      estimatedEffort: '6時間',
    },
    {
      vulnId: 'VULN-005',
      title: 'セキュリティヘッダーの不足',
      cvssScore: 4.3,
      businessImpact: 'Low',
      exploitability: 'Theoretical',
      priorityScore: 35,
      phase: 'short-term',
      assignedTeam: 'Platform Team',
      estimatedEffort: '2時間',
    },
    {
      vulnId: 'VULN-006',
      title: '詳細なエラーメッセージの露出',
      cvssScore: 3.7,
      businessImpact: 'Low',
      exploitability: 'Theoretical',
      priorityScore: 25,
      phase: 'mid-term',
      assignedTeam: 'Backend Team',
      estimatedEffort: '4時間',
    },
  ],

  phases: [
    {
      name: 'immediate',
      label: '即時対応(24時間以内)',
      deadline: '2026-02-15',
      items: [
        {
          vulnId: 'VULN-002',
          action: 'パラメータ化クエリへの移行 + WAFルール追加',
          steps: [
            '1. 商品検索エンドポイントをPrisma ORMに移行',
            '2. Zodスキーマによる入力バリデーション追加',
            '3. AWS WAFのSQLインジェクションルール有効化',
            '4. SQLインジェクション耐性テストの追加',
          ],
          rollbackPlan: '前バージョンにロールバック + WAFルールで暫定防御',
        },
        {
          vulnId: 'VULN-001',
          action: '全APIエンドポイントにオブジェクトレベル認可チェックを追加',
          steps: [
            '1. 認可ミドルウェアの実装',
            '2. 注文API、プロフィールAPIへの適用',
            '3. BOLA検証テストの追加',
          ],
          rollbackPlan: '前バージョンにロールバック',
        },
        {
          vulnId: 'VULN-003',
          action: 'プロフィール更新のスキーマバリデーション強化',
          steps: [
            '1. 更新可能フィールドのホワイトリスト定義',
            '2. Zodスキーマで不正フィールドを除外',
            '3. テストケース追加',
          ],
          rollbackPlan: '前バージョンにロールバック',
        },
      ],
    },
    {
      name: 'short-term',
      label: '短期対応(1週間以内)',
      deadline: '2026-02-21',
      items: [
        {
          vulnId: 'VULN-004',
          action: 'Redis ベースのレート制限を全エンドポイントに実装',
          steps: [
            '1. express-rate-limit + rate-limit-redis の導入',
            '2. エンドポイント別のレート制限設定',
            '3. 429応答のテストと監視ダッシュボード追加',
          ],
          rollbackPlan: 'レート制限ミドルウェアの無効化',
        },
        {
          vulnId: 'VULN-005',
          action: 'セキュリティヘッダーの追加',
          steps: [
            '1. helmetミドルウェアの導入',
            '2. CSP、HSTS、X-Frame-Options等の設定',
            '3. セキュリティヘッダーのテスト追加',
          ],
          rollbackPlan: 'helmetミドルウェアの設定変更',
        },
      ],
    },
    {
      name: 'mid-term',
      label: '中期対応(1ヶ月以内)',
      deadline: '2026-03-14',
      items: [
        {
          vulnId: 'VULN-006',
          action: 'エラーハンドリングの統一とカスタムエラーレスポンス',
          steps: [
            '1. グローバルエラーハンドラーの実装',
            '2. 本番環境でのスタックトレース非表示設定',
            '3. エラーログの集約(CloudWatch + Sentry)',
          ],
          rollbackPlan: '前バージョンにロールバック',
        },
      ],
    },
  ],

  verificationPlan: {
    perVulnerability: {
      method: '修正後に同一の攻撃パターンで再テスト',
      tools: ['OWASP ZAP再スキャン', 'カスタムテストスクリプト'],
      criteria: '同一の脆弱性が検出されないこと',
    },
    regression: {
      method: '全自動テストスイートの実行',
      tools: ['Jest', 'Playwright (E2E)', 'k6 (負荷テスト)'],
      criteria: 'テストカバレッジ80%以上、全テスト通過',
    },
    fullRescan: {
      method: '修正完了後にフルスキャンを再実行',
      tools: ['OWASP ZAP', 'Semgrep', 'Snyk'],
      criteria: 'Critical/High の新規検出がゼロ',
      schedule: '修正デプロイ後24時間以内',
    },
  },

  residualRiskAssessment: [
    {
      riskId: 'RR-001',
      description: 'ゼロデイ脆弱性(未知の脆弱性)のリスク',
      likelihood: 'Low',
      impact: 'High',
      mitigations: ['WAFの異常検知', 'ランタイム保護(RASP)', '定期スキャン'],
      accepted: true,
      acceptedBy: '佐藤CTO',
      reviewDate: '2026-05-14',
    },
    {
      riskId: 'RR-002',
      description: 'サードパーティライブラリの未発見の脆弱性',
      likelihood: 'Medium',
      impact: 'Medium',
      mitigations: ['SCAの継続実行', 'SBOM管理', 'Dependabotの自動更新'],
      accepted: true,
      acceptedBy: '佐藤CTO',
      reviewDate: '2026-05-14',
    },
  ],
};

interface RemediationPhase {
  name: string;
  label: string;
  deadline: string;
  items: {
    vulnId: string;
    action: string;
    steps: string[];
    rollbackPlan: string;
  }[];
}

interface VerificationPlan {
  perVulnerability: { method: string; tools: string[]; criteria: string };
  regression: { method: string; tools: string[]; criteria: string };
  fullRescan: { method: string; tools: string[]; criteria: string; schedule: string };
}

interface ResidualRisk {
  riskId: string;
  description: string;
  likelihood: 'High' | 'Medium' | 'Low';
  impact: 'High' | 'Medium' | 'Low';
  mitigations: string[];
  accepted: boolean;
  acceptedBy: string;
  reviewDate: string;
}

評価基準

評価項目配点合格ライン
OWASP ZAPスキャン設計25%認証済みスキャンとスキャンポリシーが適切に設計されている
SQLインジェクション評価25%攻撃パターンの理解と多層防御の設計ができている
APIセキュリティ監査25%OWASP API Top 10に基づく監査チェックリストが作成されている
修正計画の策定25%優先順位と修正ロードマップが論理的に構成されている

推定読了時間: 80分