ストーリー
佐藤CTOがプルリクエストのレビュー画面を開きました。
セキュリティコードレビューチェックリスト
観点別チェックリスト
| カテゴリ | チェック項目 | 重要度 |
|---|---|---|
| 入力バリデーション | 全ての外部入力にバリデーションが適用されているか | Critical |
| 入力バリデーション | ホワイトリスト方式で許可値を定義しているか | High |
| 入力バリデーション | ファイルアップロードの種類・サイズ制限があるか | High |
| 認証・認可 | 全エンドポイントに認証チェックがあるか | Critical |
| 認証・認可 | オブジェクトレベルの認可チェック(BOLA対策)があるか | Critical |
| 認証・認可 | ロールベースアクセス制御が正しく実装されているか | High |
| データ保護 | 機密データ(PII、クレジットカード等)がログに出力されていないか | Critical |
| データ保護 | パスワードが適切にハッシュ化されているか(bcrypt/Argon2) | Critical |
| データ保護 | 暗号化キーがソースコードにハードコードされていないか | Critical |
| エラーハンドリング | エラーレスポンスに内部情報(スタックトレース等)が含まれていないか | High |
| エラーハンドリング | 例外が適切にキャッチされ、安全なデフォルト値に置換されているか | Medium |
| SQL/NoSQL | パラメータ化クエリまたはORMを使用しているか | Critical |
| SQL/NoSQL | 動的なクエリ構築にユーザー入力が直接含まれていないか | Critical |
| 出力エンコーディング | HTML出力がコンテキストに応じてエスケープされているか | High |
| 出力エンコーディング | CSP(Content Security Policy)が設定されているか | Medium |
| 依存関係 | 新しい依存関係に既知の脆弱性がないか | High |
| 依存関係 | ライセンスの互換性が確認されているか | Medium |
静的解析ツールの統合
ツール比較
| ツール | 種別 | 言語対応 | CI統合 | 特徴 |
|---|---|---|---|---|
| Semgrep | SAST | 30+言語 | GitHub Actions, GitLab CI | カスタムルール作成が容易 |
| CodeQL | SAST | 12言語 | GitHub Advanced Security | データフロー解析に優れる |
| SonarQube | SAST + 品質 | 30+言語 | Jenkins, GitHub Actions | 品質メトリクスも統合 |
| ESLint Security | Lint | JS/TS | 全CI/CD | 軽量、高速 |
Semgrepカスタムルールの活用
# .semgrep/security-rules.yml
rules:
# ルール1: eval()の使用禁止
- id: no-eval-usage
patterns:
- pattern: eval($X)
message: |
eval()の使用は禁止されています。
任意のコード実行のリスクがあります。
代替: JSON.parse(), new Function() (それでも注意が必要)
languages: [typescript, javascript]
severity: ERROR
metadata:
cwe: "CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code"
owasp: "A03:2021 - Injection"
# ルール2: innerHTMLへの動的値の代入禁止
- id: no-dynamic-innerhtml
patterns:
- pattern: $EL.innerHTML = $VALUE
- pattern-not: $EL.innerHTML = ""
- pattern-not: $EL.innerHTML = ''
message: |
innerHTMLへの動的値の代入はXSSリスクがあります。
代替: textContent, createElement + appendChild
languages: [typescript, javascript]
severity: WARNING
metadata:
cwe: "CWE-79: Improper Neutralization of Input During Web Page Generation"
# ルール3: ハードコードされたシークレットの検出
- id: hardcoded-secret
patterns:
- pattern: |
const $KEY = "..."
- metavariable-regex:
metavariable: $KEY
regex: "(password|secret|apiKey|api_key|token|privateKey|private_key)"
message: |
シークレットがハードコードされています。
環境変数またはシークレットマネージャーを使用してください。
languages: [typescript, javascript]
severity: ERROR
metadata:
cwe: "CWE-798: Use of Hard-coded Credentials"
# ルール4: SQL文字列連結の検出
- id: sql-string-concatenation
patterns:
- pattern: |
$QUERY = `...${$VAR}...`
- metavariable-regex:
metavariable: $QUERY
regex: ".*(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE).*"
message: |
SQLクエリに変数を文字列連結しています。
SQLインジェクションのリスクがあります。
パラメータ化クエリまたはORMを使用してください。
languages: [typescript, javascript]
severity: ERROR
metadata:
cwe: "CWE-89: SQL Injection"
# ルール5: 不適切なCORS設定の検出
- id: cors-wildcard
patterns:
- pattern: |
cors({ origin: "*" })
- pattern: |
cors({ origin: true })
message: |
CORSのoriginにワイルドカードが設定されています。
許可するドメインを明示的に指定してください。
languages: [typescript, javascript]
severity: WARNING
metadata:
cwe: "CWE-942: Permissive Cross-domain Policy"
PR レビューへの自動統合
# .github/workflows/security-review.yml
name: Security Code Review
on:
pull_request:
types: [opened, synchronize]
jobs:
semgrep-review:
name: Semgrep Security Review
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Semgrep Scan
uses: semgrep/semgrep-action@v1
with:
config: >-
.semgrep/security-rules.yml
p/owasp-top-ten
p/typescript
generateSarif: "1"
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarif
eslint-security:
name: ESLint Security Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- name: ESLint Security Rules
run: npx eslint --config .eslintrc.security.js 'src/**/*.ts'
コードレビューで発見すべき脆弱性パターン
パターン1: 不適切な認証チェック
// NG: 認証チェックの欠如
app.delete('/api/v1/users/:id', async (req, res) => {
// 認証チェックなし!誰でもユーザーを削除できる
await prisma.user.delete({ where: { id: req.params.id } });
res.status(204).send();
});
// OK: 認証 + 認可チェック
app.delete('/api/v1/users/:id',
authenticate, // JWT検証
authorize('admin'), // 管理者権限チェック
async (req, res) => {
await prisma.user.delete({ where: { id: req.params.id } });
res.status(204).send();
},
);
パターン2: 機密データのログ出力
// NG: パスワードやトークンをログに出力
app.post('/api/v1/auth/login', async (req, res) => {
console.log('Login attempt:', req.body); // パスワードが含まれる!
const { email, password } = req.body;
// ...
});
// OK: 機密データをマスク
app.post('/api/v1/auth/login', async (req, res) => {
console.log('Login attempt:', { email: req.body.email });
const { email, password } = req.body;
// ...
});
// さらに良い: 構造化ログ + 自動マスク
import { logger } from './logger';
const sensitiveFields = ['password', 'token', 'creditCard', 'ssn'];
function maskSensitiveData(data: Record<string, unknown>): Record<string, unknown> {
const masked = { ...data };
for (const field of sensitiveFields) {
if (field in masked) {
masked[field] = '***REDACTED***';
}
}
return masked;
}
パターン3: パストラバーサル
// NG: ユーザー入力をファイルパスに直接使用
app.get('/api/v1/files/:filename', async (req, res) => {
const filePath = path.join('/uploads', req.params.filename);
// 攻撃: GET /api/v1/files/../../etc/passwd
res.sendFile(filePath);
});
// OK: パストラバーサル防止
import path from 'path';
app.get('/api/v1/files/:filename', async (req, res) => {
const uploadsDir = path.resolve('/uploads');
const requestedPath = path.resolve(uploadsDir, req.params.filename);
// uploadsディレクトリ外へのアクセスを防止
if (!requestedPath.startsWith(uploadsDir)) {
return res.status(403).json({ error: 'Access denied' });
}
// ファイル名のバリデーション
const safeFilename = path.basename(req.params.filename);
if (safeFilename !== req.params.filename) {
return res.status(400).json({ error: 'Invalid filename' });
}
res.sendFile(requestedPath);
});
パターン4: 安全でないデシリアライゼーション
// NG: 信頼できないデータのデシリアライゼーション
app.post('/api/v1/import', async (req, res) => {
// YAML.loadはコード実行が可能な場合がある
const data = yaml.load(req.body.yamlContent);
// ...
});
// OK: 安全なデシリアライゼーション
import yaml from 'js-yaml';
app.post('/api/v1/import', async (req, res) => {
// safeLoadは安全なスカラ型のみ許可
const data = yaml.load(req.body.yamlContent, { schema: yaml.FAILSAFE_SCHEMA });
// スキーマバリデーション
const parseResult = ImportDataSchema.safeParse(data);
if (!parseResult.success) {
return res.status(400).json({ error: 'Invalid data format' });
}
// ...
});
パターン5: 競合状態(Race Condition)
// NG: チェックと実行の間に競合状態が発生
app.post('/api/v1/transfer', async (req, res) => {
const { fromAccountId, toAccountId, amount } = req.body;
const fromAccount = await prisma.account.findUnique({
where: { id: fromAccountId },
});
if (fromAccount!.balance < amount) {
return res.status(400).json({ error: 'Insufficient funds' });
}
// この間に別のリクエストが同時に実行されるとダブルスペンド!
await prisma.account.update({
where: { id: fromAccountId },
data: { balance: { decrement: amount } },
});
await prisma.account.update({
where: { id: toAccountId },
data: { balance: { increment: amount } },
});
});
// OK: トランザクションと楽観的ロック
app.post('/api/v1/transfer', async (req, res) => {
const { fromAccountId, toAccountId, amount } = req.body;
await prisma.$transaction(async (tx) => {
// SELECT FOR UPDATE で排他ロック
const fromAccount = await tx.$queryRaw`
SELECT * FROM accounts WHERE id = ${fromAccountId} FOR UPDATE
`;
if (fromAccount[0].balance < amount) {
throw new Error('Insufficient funds');
}
await tx.account.update({
where: { id: fromAccountId },
data: { balance: { decrement: amount } },
});
await tx.account.update({
where: { id: toAccountId },
data: { balance: { increment: amount } },
});
});
res.json({ success: true });
});
セキュリティレビューの自動化と効率化
レビュワーアサイン戦略
# .github/CODEOWNERS
# セキュリティに関連するファイルはセキュリティチームがレビュー必須
# 認証・認可関連
/src/middleware/auth* @security-team
/src/services/auth* @security-team
# 暗号化・ハッシュ関連
/src/utils/crypto* @security-team
/src/utils/hash* @security-team
# データベースマイグレーション(スキーマ変更はセキュリティ影響を確認)
/prisma/migrations/ @security-team @db-admin
# インフラ設定
/infrastructure/ @security-team @platform-team
# CI/CD設定
/.github/workflows/ @security-team @platform-team
# 依存関係の変更
/package.json @security-team
/package-lock.json @security-team
セキュリティレビューのGitHub PR テンプレート
## セキュリティチェックリスト
### 変更内容に応じて該当項目をチェックしてください
#### 入力処理
- [ ] 外部入力にバリデーションを適用した
- [ ] ファイルアップロードの制限を確認した
#### 認証・認可
- [ ] 新規エンドポイントに認証チェックを追加した
- [ ] オブジェクトレベルの認可を確認した
- [ ] 管理者APIのアクセス制御を確認した
#### データ保護
- [ ] 機密データがログに出力されていないことを確認した
- [ ] シークレットがハードコードされていないことを確認した
- [ ] 暗号化の必要なデータが適切に保護されていることを確認した
#### 依存関係
- [ ] 新しい依存関係の脆弱性を確認した
- [ ] ライセンスの互換性を確認した
#### 該当しない場合
- [ ] この変更にセキュリティ上の影響はありません(理由: )
まとめ
| ポイント | 内容 |
|---|---|
| チェックリスト | カテゴリ別のセキュリティレビュー観点を標準化 |
| 静的解析統合 | Semgrep/CodeQL/SonarQube をCI/CDに組み込み、自動検出 |
| 脆弱性パターン | 認証欠如、ログ漏洩、パストラバーサル、デシリアライゼーション、競合状態 |
| 自動化 | CODEOWNERS、PRテンプレート、SARIF統合で効率化 |
| Shift Left | コードレビュー段階で発見するコストは本番発見の1/100 |
チェックリスト
- セキュリティコードレビューチェックリストを作成できる
- Semgrepのカスタムルールを作成し、CI/CDに統合できる
- 主要な脆弱性パターンをコードレビューで識別できる
- CODEOWNERSとPRテンプレートを活用した自動化ができる
- セキュリティレビューの文化をチームに浸透させる方法を説明できる
次のステップへ
次は「コンプライアンスフレームワーク」です。SOC2、ISO27001、PCI DSSなどのフレームワークを理解し、組織のコンプライアンス体制を設計しましょう。
推定読了時間: 30分