security-audit について
security-auditは、開発者やセキュリティレビュー担当者向けのコードベース監査スキルです。認証、入力処理、データ保護、依存関係まで、段階的に脆弱性や設定上のリスクを確認します。
主な機能
- 01段階的な攻撃対象領域の確認
- 02認証とアクセス制御の点検
- 03インジェクションと安全でない入力の確認
- 04機密データとシークレット管理の点検
- 05依存関係とサプライチェーンリスクの確認
活用シーン
- 01デプロイ前のセキュリティレビュー
- 02インシデントや脆弱性報告への対応
- 03大規模な機能変更後の制御確認
- 04セキュリティコンプライアンス監査の準備
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mgd34msu/ goodvibes-plugin --skill security-auditインストール後、Claude Code で/security-auditを入力して読み込まれたか確認します。
同梱ファイルreferences/security-patterns.mdscripts/validate-security-audit.sh
詳細
リソース
scripts/
validate-security-audit.sh
references/
security-patterns.md
セキュリティ監査
このスキルでは、コードベースを包括的にセキュリティ監査し、脆弱性、安全でないパターン、設定上の問題を特定する方法を説明します。セキュリティレビューの実施、本番環境へのデプロイ準備、セキュリティインシデントへの対応時に使用してください。
このスキルを使用する場面
- デプロイ前のセキュリティレビューを実施するとき
- セキュリティインシデントや脆弱性の報告に対応するとき
- 既存のコードベースに対して定期的なセキュリティ監査を実施するとき
- 大規模な機能追加後にセキュリティ制御を検証するとき
- セキュリティコンプライアンス監査(SOC 2、ISO 27001)に備えるとき
- 新しいチームメンバーにセキュリティ標準を説明するとき
監査の手法
体系的なセキュリティ監査は、次のフェーズに沿って進めます。
フェーズ 1:偵察
目的: アプリケーションのアーキテクチャ、技術スタック、攻撃対象領域を把握します。
precision_grep を使ってコードベースを調査します:
precision_grep:
queries:
- id: auth_patterns
pattern: "(session|token|auth|login|password|jwt)"
glob: "**/*.{ts,tsx,js,jsx}"
- id: api_endpoints
pattern: "(router\\.(get|post|put|delete)|export.*GET|export.*POST)"
glob: "**/*.{ts,tsx,js,jsx}"
- id: database_queries
pattern: "(prisma\\.|db\\.|query\\(|execute\\()"
glob: "**/*.{ts,tsx,js,jsx}"
- id: env_usage
pattern: "process\\.env\\."
glob: "**/*.{ts,tsx,js,jsx}"
output:
format: minimal
重要なコンポーネントを特定します:
- 認証フロー(ログイン、登録、パスワードのリセット)
- 認可ミドルウェアとガード
- API エンドポイント(公開か認証必須か)
- データベースへのアクセスパターン
- ファイルのアップロード/ダウンロードハンドラー
- 決済処理ロジック
- 管理パネルや特権操作
フェーズ 2:認証監査
目的: 認証が安全に実装されていることを確認します。
脆弱なセッション管理を確認する
セッション設定の問題を検索します:
precision_grep:
queries:
- id: session_config
pattern: "(session|cookie).*secure.*false|httpOnly.*false|sameSite.*(none|lax)"
glob: "**/*.{ts,tsx,js,jsx}"
- id: jwt_secrets
pattern: "jwt\\.sign.*secret.*[\"'][^\"']{1,20}[\"']|new.*JwtStrategy"
glob: "**/*.{ts,tsx,js,jsx}"
output:
format: standard
よくある脆弱性:
httpOnlyフラグのないセッション Cookie(XSS にさらされます)secureフラグのないセッション Cookie(HTTP 経由での送信が可能になります)- 脆弱な JWT シークレット(エントロピーが 32 バイト未満)
- 有効期限のないセッション(無期限のセッション)
- ログアウト時にセッションが無効化されない
安全なセッションの例:
import { cookies } from 'next/headers';
export async function createSession(userId: string) {
const sessionToken = await generateSecureToken();
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
await db.session.create({
data: {
token: sessionToken,
userId,
expiresAt,
},
});
cookies().set('session', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
expires: expiresAt,
path: '/',
});
}
export async function invalidateSession(sessionToken: string) {
await db.session.delete({ where: { token: sessionToken } });
cookies().delete('session');
}
パスワードのセキュリティ問題を確認する
脆弱なパスワード処理を検索します:
precision_grep:
queries:
- id: password_storage
pattern: "password.*=.*(req\\.body|params|query)|password.*toString|password.*text"
glob: "**/*.{ts,tsx,js,jsx}"
- id: password_hashing
pattern: "(bcrypt|argon2|scrypt|pbkdf2)"
glob: "**/*.{ts,tsx,js,jsx}"
output:
format: verbose
よくある脆弱性:
- パスワードが平文で保存されている
- 脆弱なハッシュアルゴリズム(MD5、SHA1、ソルトなしの SHA256)
- パスワードの複雑さに関する要件がない
- パスワードのヒントや秘密の質問
- パスワードがログに記録されたり、エラーメッセージに露出したりする
安全なパスワードハッシュ化:
import { hash, verify } from '@node-rs/argon2';
export async function hashPassword(password: string): Promise<string> {
return await hash(password, {
memoryCost: 19456, // 19 MB
timeCost: 2,
outputLen: 32,
parallelism: 1,
});
}
export async function verifyPassword(
hash: string,
password: string
): Promise<boolean> {
try {
return await verify(hash, password);
} catch {
return false;
}
}
MFA の実装を確認する
MFA のパターンを検索します:
precision_grep:
queries:
- id: mfa_usage
pattern: "(totp|authenticator|2fa|mfa|otp)"
glob: "**/*.{ts,tsx,js,jsx}"
output:
format: minimal
MFA のベストプラクティス:
- 認証アプリを使用した TOTP(時間ベースのワンタイムパスワード)
- アカウント復旧用のバックアップコード
- フォールバックとしての SMS OTP(主要な手段にはしない)
- ハードウェアキー向けの WebAuthn/FIDO2
- OTP 検証に対するレート制限
フェーズ 3:認可監査
目的: 適切なアクセス制御と権限チェックを徹底します。
認可チェックの欠落を確認する
API エンドポイントを特定します:
precision_grep:
queries:
- id: api_routes
pattern: "export async function (GET|POST|PUT|DELETE|PATCH)"
glob: "**/api/**/*.{ts,tsx,js,jsx}"
- id: auth_middleware
pattern: "(requireAuth|withAuth|authorize|checkPermission)"
glob: "**/*.{ts,tsx,js,jsx}"
output:
format: locations
手動レビューのチェックリスト:
- 認証必須のすべてのエンドポイントでユーザーの身元を検証していますか?
- エンドポイントでリソースの所有者を確認していますか(ユーザーは自分のデータにのみアクセスできますか)?
- 管理者向けルートはロールチェックで保護されていますか?
- 認可はサーバー側でチェックされていますか(クライアント側だけではありませんか)?
よくある脆弱性:
- 安全でない直接オブジェクト参照(IDOR):
/api/users/123が任意のユーザーを返す - 権限昇格:一般ユーザーが管理者機能にアクセスできる
- 認可の欠落:エンドポイントがクライアント側のチェックだけに依存している
安全な認可パターン:
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
import { NextResponse } from 'next/server';
export async function GET(
req: Request,
{ params }: { params: { id: string } }
) {
const session = await auth();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const post = await db.post.findUnique({
where: { id: params.id },
select: { id: true, title: true, content: true, authorId: true },
});
if (!post) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
// Check ownership
if (post.authorId !== session.user.id) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
return NextResponse.json(post);
}
ロールベースアクセス制御(RBAC)を確認する
ロール定義を検索します:
precision_grep:
queries:
- id: role_checks
pattern: "(role.*===|role.*includes|hasRole|checkRole|permissions)"
glob: "**/*.{ts,tsx,js,jsx}"
output:
format: matches
RBAC の実装:
type Role = 'user' | 'admin' | 'moderator';
type Permission =
| 'posts:read'
| 'posts:write'
| 'posts:delete'
| 'users:manage'
| 'settings:admin';
const rolePermissions: Record<Role, Permission[]> = {
user: ['posts:read', 'posts:write'],
moderator: ['posts:read', 'posts:write', 'posts:delete'],
admin: ['posts:read', 'posts:write', 'posts:delete', 'users:manage', 'settings:admin'],
};
export function hasPermission(role: Role, permission: Permission): boolean {
return rolePermissions[role].includes(permission);
}
export function requirePermission(permission: Permission) {
return async (req: Request) => {
const session = await auth();
if (!session || !hasPermission(session.user.role, permission)) {
throw new Error('Insufficient permissions');
}
};
}
フェーズ 4:入力検証監査
目的: インジェクション攻撃や悪意のある入力を防ぎます。
SQL インジェクションを確認する
安全でないデータベースクエリを検索します:
precision_grep:
queries:
- id: raw_sql
pattern: "(\\$executeRaw|\\$queryRaw|db\\.query|connection\\.query).*\\$\\{|.*`.*\\$\\{"
glob: "**/*.{ts,tsx,js,jsx}"
- id: string_concatenation
pattern: "(SELECT|INSERT|UPDATE|DELETE).*\\+.*req\\.(body|query|params)"
glob: "**/*.{ts,tsx,js,jsx}"
output:
format: verbose
よくある脆弱性:
- SQL クエリでの文字列連結
- 生 SQL での未サニタイズのユーザー入力
- ユーザー入力に基づく動的なテーブル名/カラム名
安全なデータベースクエリ:
// UNSAFE - SQL injection vulnerable
export async function getUserByEmail(email: string) {
const query = `SELECT * FROM users WHERE email = '${email}'`;
return await db.$queryRawUnsafe(query);
}
// SAFE - Parameterized query
export async function getUserByEmail(email: string) {
return await db.user.findUnique({
where: { email },
});
}
// SAFE - Raw query with parameters
export async function searchUsers(query: string) {
return await db.$queryRaw`
SELECT id, name, email
FROM users
WHERE name ILIKE ${'%' + query + '%'}
LIMIT 20
`;
}
XSS(クロスサイトスクリプティング)を確認する
安全でないレンダリングを検索します:
precision_grep:
queries:
- id: dangerous_html
pattern: "(dangerouslySetInnerHTML|innerHTML|outerHTML)"
glob: "**/*.{tsx,jsx}"
- id: unescaped_output
pattern: "(v-html|\\[innerHTML\\])"
glob: "**/*.{vue,html}"
output:
format: locations
よくある脆弱性:
dangerouslySetInnerHTMLを使って未サニタイズのユーザー入力をレンダリングするinnerHTMLを使ってユーザー提供のコンテンツを挿入する- テンプレートエンジンで自動エスケープを無効にする
安全なレンダリング:
import DOMPurify from 'isomorphic-dompurify';
// UNSAFE - XSS vulnerable
export function UnsafeComment({ content }: { content: string }) {
return <div dangerouslySetInnerHTML={{ __html: content }} />;
}
// SAFE - Auto-escaped by React
export function SafeComment({ content }: { content: string }) {
return <div>{content}</div>;
}
// SAFE - Sanitized HTML if needed
export function SafeRichComment({ html }: { html: string }) {
const sanitized = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a'],
ALLOWED_ATTR: ['href'],
});
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}
コマンドインジェクションの確認
シェルコマンドの実行箇所を検索します:
precision_grep:
queries:
- id: shell_exec
pattern: "(exec|spawn|execSync|spawnSync|execFile).*req\\.(body|query|params)"
glob: "**/*.{ts,tsx,js,jsx}"
output:
format: verbose
よくある脆弱性:
- ユーザー入力をシェルコマンドに直接渡す
- ファイル操作でサニタイズされていないファイルパスを使用する
安全なコマンド実行:
import { spawn } from 'child_process';
import { z } from 'zod';
const allowedCommands = ['convert', 'resize', 'compress'] as const;
const commandSchema = z.enum(allowedCommands);
// UNSAFE - Command injection vulnerable
export async function processImage(filename: string) {
exec(`convert ${filename} output.png`);
}
// SAFE - Validated input and array arguments
export async function processImage(command: string, filename: string) {
const validCommand = commandSchema.parse(command);
const sanitizedFilename = filename.replace(/[^a-zA-Z0-9._-]/g, '');
return new Promise((resolve, reject) => {
const child = spawn('imagemagick', [validCommand, sanitizedFilename, 'output.png']);
child.on('exit', (code) => {
if (code === 0) resolve(undefined);
else reject(new Error('Processing failed'));
});
});
}
パストラバーサルの確認
ファイル操作を検索します:
precision_grep:
queries:
- id: file_operations
pattern: "(readFile|writeFile|unlink|stat|createReadStream).*req\\.(body|query|params)"
glob: "**/*.{ts,tsx,js,jsx}"
output:
format: locations
よくある脆弱性:
- 検証せずにユーザー制御のファイルパスを使用する
- パスの正規化が不足している(
../の並びを許可する)
安全なファイル処理:
import path from 'path';
import fs from 'fs/promises';
const UPLOADS_DIR = path.join(process.cwd(), 'uploads');
// UNSAFE - Path traversal vulnerable
export async function getFile(filename: string) {
return await fs.readFile(`./uploads/${filename}`);
}
// SAFE - Path validation with defense against encoded traversals
export async function getFile(filename: string) {
// Decode URL-encoded sequences (e.g., %2e%2e%2f -> ../)
const decoded = decodeURIComponent(filename);
const safePath = path.normalize(decoded).replace(/^(\.\.\/)+/, '');
const fullPath = path.resolve(path.join(UPLOADS_DIR, safePath));
// Ensure resolved path is within UPLOADS_DIR (prevents prefix collision)
if (!fullPath.startsWith(UPLOADS_DIR + path.sep) && fullPath !== UPLOADS_DIR) {
throw new Error('Invalid file path');
}
return await fs.readFile(fullPath);
}
フェーズ 5:データ保護の監査
目的: 機密データが暗号化され、適切に扱われていることを確認します。
保存データの暗号化を確認する
機密データの保存箇所を検索します:
precision_grep:
queries:
- id: sensitive_fields
pattern: "(ssn|credit.*card|bank.*account|passport|drivers.*license)"
glob: "**/*.{ts,tsx,js,jsx,prisma}"
- id: encryption_usage
pattern: "(encrypt|decrypt|cipher|crypto)"
glob: "**/*.{ts,tsx,js,jsx}"
output:
format: minimal
暗号化のベストプラクティス:
- 共通鍵暗号には AES-256-GCM を使用します
- 暗号化キーは安全なキー管理サービス(AWS KMS、HashiCorp Vault)に保管します
- 暗号化キーをバージョン管理にコミットしないでください
- 暗号化キーを定期的にローテーションします
安全な暗号化の実装:
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
const ALGORITHM = 'aes-256-gcm';
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY;
if (!ENCRYPTION_KEY) throw new Error('ENCRYPTION_KEY environment variable is required');
const KEY = Buffer.from(ENCRYPTION_KEY, 'hex'); // 32 bytes
export function encrypt(plaintext: string): string {
const iv = randomBytes(16);
const cipher = createCipheriv(ALGORITHM, KEY, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Return: iv:authTag:ciphertext
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}
export function decrypt(ciphertext: string): string {
const [ivHex, authTagHex, encrypted] = ciphertext.split(':');
const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const decipher = createDecipheriv(ALGORITHM, KEY, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
PII の取り扱いを確認する
個人を特定できる情報を検索します:
precision_grep:
queries:
- id: pii_fields
pattern: "(email|phone|address|name|dob|birth.*date)"
glob: "**/*.prisma"
- id: logging_pii
pattern: "(console\\.log|logger\\.(info|debug|warn)).*\\.(email|phone|ssn)"
glob: "**/*.{ts,tsx,js,jsx}"
output:
format: locations
PII 保護チェックリスト:
- PII の収集を最小限に抑えます(必要なものだけを収集します)
- ログやエラーメッセージ内の PII をマスキングします
- データ保持ポリシーを実装します(古いデータを自動削除します)
- ユーザーデータのエクスポート機能を提供します(GDPR/CCPA に準拠)
- ユーザーデータの削除機能を提供します(忘れられる権利)
シークレット管理を確認する
ハードコードされたシークレットを検索します:
precision_grep:
queries:
- id: hardcoded_secrets
pattern: "(api.*key.*=.*[\"'][a-zA-Z0-9]{20,}|secret.*=.*[\"'][a-zA-Z0-9]{20,}|password.*=.*[\"'][^\"']{8,})"
glob: "**/*.{ts,tsx,js,jsx}"
- id: committed_env
pattern: ".*"
glob: ".env"
output:
format: verbose
よくある脆弱性:
- API キーがソースコードにハードコードされている
.envファイルがバージョン管理にコミットされている- シークレットがクライアント側のコードに露出している
- デフォルトの認証情報が変更されていない
安全なシークレット管理:
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
OPENAI_API_KEY: z.string().startsWith('sk-'),
});
export const env = envSchema.parse({
DATABASE_URL: process.env.DATABASE_URL,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
});
// Ensure .env is in .gitignore
// Use .env.example (without values) to document required variables
// Use secrets management in production (Vercel Env Vars, AWS Secrets Manager)
フェーズ 6:依存関係の監査
目的: 脆弱な依存関係を特定し、修正します。
脆弱性スキャナーを自動実行する
npm audit を使用します:
precision_exec:
commands:
- cmd: "npm audit --json"
timeout_ms: 30000
verbosity: standard
古いパッケージを確認します:
precision_exec:
commands:
- cmd: "npm outdated --json"
timeout_ms: 10000
verbosity: minimal
修正の優先順位:
- 重大/高: デプロイ前に直ちに修正します
- 中: 30 日以内に修正します
- 低: 定期メンテナンス時に修正します
サプライチェーン攻撃を確認する
ロックファイルの整合性を検証します:
precision_exec:
commands:
- cmd: "npm audit signatures"
timeout_ms: 30000
verbosity: standard
チェックリスト:
- ロックファイル(
package-lock.json、yarn.lock、pnpm-lock.yaml)をコミットします - PR で依存関係の変更を確認します
- CI/CD では
npm installではなくnpm ciを使用します - 重要なプロジェクトでは依存関係のバージョンを固定します
- 依存関係の分析には Socket.dev などのツールを使用します
フェーズ 7:API セキュリティの監査
目的: 一般的な攻撃から API エンドポイントを保護します。
レート制限を確認する
レート制限の実装箇所を検索します:
precision_grep:
queries:
- id: rate_limit_usage
pattern: "(rateLimit|rate.*limiter|Ratelimit|upstash.*ratelimit)"
glob: "**/*.{ts,tsx,js,jsx}"
output:
format: minimal
レート制限を実装します:
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
import { NextResponse } from 'next/server';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
analytics: true,
});
export async function POST(req: Request) {
const ip = req.headers.get('x-forwarded-for') ?? 'unknown';
const { success, limit, reset, remaining } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': new Date(reset).toISOString(),
},
}
);
}
// Process request
return NextResponse.json({ success: true });
}
CORS 設定を確認する
CORS の設定箇所を検索します:
precision_grep:
queries:
- id: cors_config
pattern: "(Access-Control-Allow-Origin|cors\\(|corsOptions)"
glob: "**/*.{ts,tsx,js,jsx}"
output:
format: matches
よくある脆弱性:
- 認証情報を使用している状態でのワイルドカード CORS(
Access-Control-Allow-Origin: *) - 許可するオリジンのホワイトリストが広すぎる
- プリフライトリクエストの処理が欠けている
安全な CORS 設定:
import { NextResponse } from 'next/server';
const ALLOWED_ORIGINS = [
'https://example.com',
'https://app.example.com',
];
export async function GET(req: Request) {
const origin = req.headers.get('origin');
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (origin && ALLOWED_ORIGINS.includes(origin)) {
headers['Access-Control-Allow-Origin'] = origin;
headers['Access-Control-Allow-Credentials'] = 'true';
}
return NextResponse.json({ data: 'response' }, { headers });
}
Webhook 署名の検証
ベストプラクティス: タイミング攻撃を防ぐため、Webhook 署名の比較には crypto.timingSafeEqual() を使用します:
import crypto from 'crypto';
export function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {
const expectedSignature = crypto.createHmac('sha256', secret).update(payload).digest('hex');
const signatureBuffer = Buffer.from(signature);
const expectedBuffer = Buffer.from(expectedSignature);
// Prevent timing attacks - constant-time comparison
if (signatureBuffer.length !== expectedBuffer.length) return false;
return crypto.timingSafeEqual(signatureBuffer, expectedBuffer);
}
CORS プリフライトの処理
export async function OPTIONS(req: Request) {
const origin = req.headers.get('origin');
const headers: Record<string, string> = {
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400', // 24 hours
};
if (origin && ALLOWED_ORIGINS.includes(origin)) {
headers['Access-Control-Allow-Origin'] = origin;
headers['Access-Control-Allow-Credentials'] = 'true';
}
return new NextResponse(null, { status: 204, headers });
}
CSRF 保護を確認する
CSRF の実装箇所を検索します:
precision_grep:
queries:
- id: csrf_tokens
pattern: "(csrf|csrfToken|xsrf)"
glob: "**/*.{ts,tsx,js,jsx}"
output:
format: minimal
CSRF 保護の方法:
- SameSite Cookie(
sameSite: 'strict'または'lax')を使用します - 状態を変更する操作には CSRF トークンを実装します
- API リクエストにカスタムヘッダーを必須とします(例:
X-Requested-With) OriginヘッダーとRefererヘッダーを検証します
コンテンツセキュリティポリシー(CSP)を確認する
CSP ヘッダーを検索します:
precision_grep:
queries:
- id: csp_headers
pattern: "Content-Security-Policy"
glob: "**/*.{ts,tsx,js,jsx,json}"
output:
format: locations
CSP を実装します:
// next.config.js
const ContentSecurityPolicy = `
default-src 'self';
// WARNING: 'unsafe-eval' and 'unsafe-inline' significantly weaken CSP. Use nonces or hashes instead.
script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.vercel-insights.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
`;
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: ContentSecurityPolicy.replace(/\n/g, ''),
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
],
},
];
},
};
フェーズ 8:インフラストラクチャセキュリティの監査
目的: デプロイインフラストラクチャを強化します。
Docker セキュリティを確認する
Dockerfile を検索する:
precision_read:
files:
- path: "Dockerfile"
extract: content
verbosity: minimal
Docker セキュリティチェックリスト:
- 最小限のベースイメージ(alpine、distroless)を使用する
- 非 root ユーザーとして実行する
- マルチステージビルドを使用して攻撃対象領域を縮小する
- イメージレイヤーにシークレットを含めない(ビルド引数を使用する)
- ベースイメージのバージョンを固定する(
latestタグは避ける) - イメージの脆弱性をスキャンする(
docker scan、Trivy)
安全な Dockerfile:
FROM node:20-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Create non-root user
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
CMD ["node", "server.js"]
環境変数のセキュリティを確認する
環境変数ファイルの使用状況を検索する:
precision_glob:
patterns:
- ".env*"
- "*.env"
verbosity: count_only
.gitignore を確認する:
precision_read:
files:
- path: ".gitignore"
extract: content
verbosity: minimal
環境変数のセキュリティチェックリスト:
.envファイルを.gitignoreに含める- 変数を文書化するために、値を含まない
.env.exampleを使用する - 本番環境ではシークレット管理を使用する(
.envファイルは使用しない) - 環境を分離する(開発、ステージング、本番)
- シークレットを定期的にローテーションする
TLS 設定を確認する
HTTPS の強制を確認する:
precision_grep:
queries:
- id: https_redirect
pattern: "(https|ssl|tls|hsts|Strict-Transport-Security)"
glob: "**/*.{ts,tsx,js,jsx,json}"
output:
format: minimal
TLS のベストプラクティス:
- HTTPS を強制する(HTTP を HTTPS にリダイレクトする)
- HSTS(Strict-Transport-Security)を有効にする
- TLS 1.2 以降を使用する(TLS 1.0、1.1 は無効にする)
- 強力な暗号スイートを使用する
- モバイルアプリには証明書ピンニングを実装する
監査レポート
監査結果は次の形式でまとめてください。
エグゼクティブサマリー
## Security Audit Report
**Application:** [Name]
**Audit Date:** [Date]
**Auditor:** [Name/Team]
**Scope:** [Components audited]
### Summary
- Total findings: X
- Critical: Y
- High: Z
- Medium: A
- Low: B
### Risk Assessment
[Overall risk level: Critical/High/Medium/Low]
詳細な調査結果
脆弱性ごとに、次の項目を記載します。
### [SEVERITY] Finding #X: [Title]
**Category:** [Authentication/Authorization/Input Validation/etc.]
**Severity:** [Critical/High/Medium/Low]
**CWE:** [CWE-XXX if applicable]
**Description:**
[Clear description of the vulnerability]
**Location:**
- File: `path/to/file.ts`
- Lines: 42-58
**Impact:**
[What can an attacker do? What data is at risk?]
**Proof of Concept:**
```typescript
// Example exploit code or reproduction steps
修正方法:
// Secure code example
参考資料:
- [OWASP リンク]
- [CWE リンク]
### Severity Classification
| Severity | Criteria |
|----------|----------|
| **Critical** | Remote code execution, authentication bypass, sensitive data exposure |
| **High** | Privilege escalation, SQL injection, XSS in critical flows |
| **Medium** | Information disclosure, CSRF, weak authentication |
| **Low** | Minor information leaks, missing security headers |
## Precision Tool Workflows
### Full Security Scan Workflow
Run a comprehensive security scan using precision_grep:
```yaml
precision_grep:
queries:
# Authentication issues
- id: weak_auth
pattern: "(password.*plain|password.*clear|md5|sha1)\\("
glob: "**/*.{ts,tsx,js,jsx}"
# SQL injection
- id: sql_injection
pattern: "(\\$queryRaw|\\$executeRaw).*\\$\\{|query.*\\+.*params"
glob: "**/*.{ts,tsx,js,jsx}"
# XSS
- id: xss
pattern: "(dangerouslySetInnerHTML|innerHTML|v-html)"
glob: "**/*.{tsx,jsx,vue}"
# Command injection
- id: command_injection
pattern: "(exec|spawn).*\\(.*req\\.(body|query)"
glob: "**/*.{ts,tsx,js,jsx}"
# Path traversal
- id: path_traversal
pattern: "(readFile|writeFile).*req\\.(body|query|params)"
glob: "**/*.{ts,tsx,js,jsx}"
# Hardcoded secrets
- id: hardcoded_secrets
pattern: "(api.*key.*=.*[\"'][a-zA-Z0-9]{20,}|sk_live)"
glob: "**/*.{ts,tsx,js,jsx}"
# Insecure cookies
- id: insecure_cookies
pattern: "(httpOnly.*false|secure.*false|sameSite.*none)"
glob: "**/*.{ts,tsx,js,jsx}"
output:
format: locations
一括セキュリティ監査
効率的に監査するには、discover + batch を使用します。
discover:
queries:
- id: auth_files
type: glob
patterns: ["**/auth/**/*.{ts,tsx,js,jsx}", "**/api/auth/**/*.{ts,tsx,js,jsx}"]
- id: api_routes
type: glob
patterns: ["**/api/**/*.{ts,tsx,js,jsx}"]
- id: db_files
type: grep
pattern: "(prisma|db|database)"
glob: "**/*.{ts,tsx,js,jsx}"
verbosity: files_only
次に、batch で読み取って分析します。
precision_read:
files: [/* Use discovered files */]
extract: symbols
symbol_filter: ["function", "class"]
verbosity: standard
自動セキュリティテスト
セキュリティチェックを CI/CD に統合します。
# .github/workflows/security.yml
name: Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run npm audit
run: npm audit --audit-level=high
- name: Run Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'CRITICAL,HIGH'
- name: Run OWASP ZAP scan
uses: zaproxy/[email protected]
with:
target: 'http://localhost:3000'
よくあるセキュリティ上の間違い
1. クライアント側の検証を信頼する
問題: セキュリティをクライアント側のチェックに依存しています。
解決策: 必ずサーバー側で検証します。
// UNSAFE - Client-side only
export function ClientForm() {
const [email, setEmail] = useState('');
const isValid = email.includes('@');
return (
<form onSubmit={() => fetch('/api/subscribe', {
method: 'POST',
body: JSON.stringify({ email }),
})}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<button disabled={!isValid}>Subscribe</button>
</form>
);
}
// SAFE - Server-side validation
export async function POST(req: Request) {
const body = await req.json();
const schema = z.object({
email: z.string().email(),
});
const result = schema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: result.error.flatten() },
{ status: 400 }
);
}
// Process valid email
await subscribeUser(result.data.email);
return NextResponse.json({ success: true });
}
2. API レスポンスで機密データを公開する
問題: 必要以上のデータを返しています。
解決策: 明示的に select を使用してフィールドを制限します。
// UNSAFE - Returns password hash
export async function GET(req: Request) {
const user = await db.user.findUnique({ where: { id: userId } });
return NextResponse.json(user);
}
// SAFE - Excludes sensitive fields
export async function GET(req: Request) {
const user = await db.user.findUnique({
where: { id: userId },
select: {
id: true,
name: true,
email: true,
avatar: true,
// passwordHash excluded
},
});
return NextResponse.json(user);
}
3. 機密データをログに記録する
問題: 個人識別情報(PII)やシークレットをログに書き込んでいます。
解決策: ログに記録する前に機密データをマスキングします。
// UNSAFE - Logs password
export async function login(email: string, password: string) {
console.log('Login attempt:', { email, password });
// ...
}
// SAFE - Redacts password
export async function login(email: string, password: string) {
console.log('Login attempt:', { email, password: '[REDACTED]' });
// ...
}
監査後の対応
監査の完了後、次の作業を行います。
- 重大度と悪用されやすさに基づいて調査結果に優先順位を付ける
- 調査結果ごとにチケットを作成する(監査レポートへのリンクを含める)
- 重大度がクリティカルまたは高の問題を直ちに修正する
- 検証スクリプトを実行して修正を確認する
- 修正後にフォローアップ監査を予定する
- 得られた教訓を反映してセキュリティドキュメントを更新する
- 発見されたよくある脆弱性についてチームをトレーニングする
検証
監査での修正後に検証スクリプトを実行します。
./scripts/validate-security-audit.sh /path/to/project
スクリプトでは、次の項目を確認します。
- よくある脆弱性のパターン
- 安全な認証の実装
- 入力検証の適用範囲
- シークレット管理
- セキュリティヘッダーの設定
- 依存関係の脆弱性