LESSON 30分

セキュリティ視点のコードレビュー

ストーリー

「脆弱なコードパターンは分かったか?」高橋さんが確認する。

「はい。文字列連結、エスケープ漏れ、ハードコードされたシークレット......」

「よし。次はそれをコードレビューで実際に見つける方法だ。 パターンを知っているだけでは不十分で、何を、どこを、どう見るかを体系化する必要がある」

「チェックリストのようなものですか?」

「まさにそうだ。セキュリティレビューのチェックリストを作って、漏れなく確認できるようにしよう」


セキュリティレビューの心構え

攻撃者の視点で考える

通常のコードレビューは「このコードは正しく動くか」を確認しますが、セキュリティレビューでは「このコードは悪意ある入力でどう動くか」を考えます。

通常のレビュー:
  「正しい入力に対して、期待通りの結果が返るか?」

セキュリティレビュー:
  「悪意ある入力に対して、安全に処理されるか?」
  「認証・認可のチェックは漏れていないか?」
  「エラー時に内部情報が漏洩しないか?」

セキュリティレビューチェックリスト

1. 入力処理

チェック項目:
□ ユーザー入力は全てバリデーションされているか
□ バリデーションはサーバーサイドで行われているか(クライアントだけでは不十分)
□ 型チェック(数値、文字列、日付など)が行われているか
□ 長さの制限が設けられているか
□ ホワイトリスト方式が使われているか(ブラックリストより安全)
typescript
// 悪い例: クライアントサイドのみのバリデーション
<input type="email" required>  <!-- ブラウザの開発者ツールで簡単にバイパスされる -->

// 良い例: サーバーサイドでもバリデーション
import { z } from 'zod';

const userSchema = z.object({
  email: z.string().email().max(255),
  name: z.string().min(1).max(100),
  age: z.number().int().min(0).max(150),
});

app.post('/users', (req, res) => {
  const result = userSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ errors: result.error.issues });
  }
  // result.data は型安全かつバリデーション済み
});

2. データベース操作

チェック項目:
□ パラメータ化クエリが使われているか
□ 文字列連結でSQLを構築していないか
□ ORMを使用している場合、raw queryに入力値を直接埋め込んでいないか
□ データベース接続ユーザーの権限は最小限か
typescript
// レビューで見つけるべき危険なパターン
const result = await prisma.$queryRawUnsafe(
  `SELECT * FROM users WHERE name = '${name}'`  // 危険
);

// 安全なパターン
const result = await prisma.$queryRaw`
  SELECT * FROM users WHERE name = ${name}
`;  // Prisma がパラメータ化してくれる

3. 出力処理

チェック項目:
□ HTMLに出力する際にエスケープされているか
□ innerHTML の使用箇所はないか
□ テンプレートエンジンの自動エスケープが有効か
□ JSONレスポンスに不要な情報が含まれていないか

4. 認証・認可

チェック項目:
□ 全てのAPIエンドポイントに認証チェックがあるか
□ リソースへのアクセスに認可チェック(権限確認)があるか
□ IDの直接参照による不正アクセスが防がれているか
□ パスワードは適切にハッシュ化されているか
□ セッション管理は安全か
typescript
// 危険: 認可チェックの漏れ
app.get('/api/orders/:id', authMiddleware, async (req, res) => {
  const order = await db.getOrder(req.params.id);
  res.json(order);
  // 認証はしているが、他人の注文も見れてしまう
});

// 安全: 認可チェック付き
app.get('/api/orders/:id', authMiddleware, async (req, res) => {
  const order = await db.getOrder(req.params.id);
  if (order.userId !== req.user.id) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  res.json(order);
});

5. エラー処理とログ

チェック項目:
□ エラーメッセージに内部情報が含まれていないか
□ スタックトレースがクライアントに返されていないか
□ セキュリティ関連のイベントがログに記録されているか
□ ログに機密情報(パスワード、トークン)が出力されていないか

6. 依存関係とファイル

チェック項目:
□ .env ファイルがコミットされていないか
□ シークレットがソースコードにハードコードされていないか
□ package-lock.json(yarn.lock)がコミットされているか
□ 既知の脆弱性を持つパッケージがないか

レビューの進め方

Step-by-Step

1. データの流れを追う
   入力 → 処理 → 出力 の各段階で脆弱性がないか確認

2. 境界を確認する
   外部システム(DB、API、ファイルシステム)との接点を重点的にチェック

3. 認証・認可のチェーン
   全てのエンドポイントで認証→認可の流れが途切れていないか確認

4. エラーパスを確認
   正常系だけでなく、異常系(エラー時)の挙動もチェック

データフロー図で考える

ユーザー入力
    │
    ▼
[バリデーション] ← ここで不正な入力を排除しているか?
    │
    ▼
[ビジネスロジック] ← 認可チェックは適切か?
    │
    ▼
[データベース操作] ← パラメータ化クエリか?
    │
    ▼
[レスポンス生成] ← エスケープしているか?不要な情報はないか?
    │
    ▼
クライアント

レビューコメントの書き方

セキュリティ問題を指摘するとき

悪い例:
「このコードは危険です」  ← 具体性がない

良い例:
「この箇所では req.params.id がバリデーションなしに
SQL クエリに埋め込まれています。攻撃者が "1 OR 1=1" のような
値を送信すると、全レコードが返される可能性があります。
パラメータ化クエリに変更してください。

修正案:
const result = await db.query('SELECT * FROM users WHERE id = $1', [id]);」

重要度の分類

レベル説明
CRITICAL即座に修正が必要SQLインジェクション、認証バイパス
HIGHマージ前に修正すべきXSS、ハードコードされたシークレット
MEDIUM早めに対応すべき不適切なエラーメッセージ、弱い入力バリデーション
LOW改善が望ましいベストプラクティスからの逸脱

まとめ

ポイント内容
視点攻撃者の目線でコードを読む
チェックリスト入力、DB、出力、認証、エラー、依存関係の6領域
データフロー入力から出力までの流れを追って脆弱性を探す
コメント具体的な攻撃シナリオと修正案を示す

チェックリスト

  • セキュリティレビューの6つの領域を理解した
  • データフローに沿ったレビュー方法を理解した
  • レビューコメントに攻撃シナリオと修正案を含める重要性を理解した

次のステップへ

コードレビューの手法を学びました。 次のセクションでは、人手によるレビューを補完する脆弱性スキャンツールを学びます。

ツールを活用することで、見落としのリスクを減らせます。


推定読了時間: 30分