セキュリティ視点のコードレビュー
ストーリー
「脆弱なコードパターンは分かったか?」高橋さんが確認する。
「はい。文字列連結、エスケープ漏れ、ハードコードされたシークレット......」
「よし。次はそれをコードレビューで実際に見つける方法だ。 パターンを知っているだけでは不十分で、何を、どこを、どう見るかを体系化する必要がある」
「チェックリストのようなものですか?」
「まさにそうだ。セキュリティレビューのチェックリストを作って、漏れなく確認できるようにしよう」
セキュリティレビューの心構え
攻撃者の視点で考える
通常のコードレビューは「このコードは正しく動くか」を確認しますが、セキュリティレビューでは「このコードは悪意ある入力でどう動くか」を考えます。
通常のレビュー:
「正しい入力に対して、期待通りの結 果が返るか?」
セキュリティレビュー:
「悪意ある入力に対して、安全に処理されるか?」
「認証・認可のチェックは漏れていないか?」
「エラー時に内部情報が漏洩しないか?」
セキュリティレビューチェックリスト
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分