security について
Inbox Zero のコードレビュー向けセキュリティガイドスキルです。ルート認証、ユーザー単位のデータアクセス、入力検証、安全なエラー処理、cron 保護を扱います。
主な機能
- 01認証ミドルウェアの選択
- 02ユーザー・アカウント単位のデータ制限
- 03リソース所有権の確認
- 04入力検証とサニタイズ
- 05安全なエラー処理
- 06cron エンドポイントの認証
活用シーン
- 01API ルートの認証方式のレビュー
- 02データベースクエリのユーザー制限確認
- 03IDOR を想定した所有権保護のテスト
- 04入力処理とエラー情報漏えいの監査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ elie222/ inbox-zero --skill securityインストール後、Claude Code で/securityを入力して読み込まれたか確認します。
詳細
セキュリティガイドライン
重要なセキュリティルール
🚨 これらのセキュリティ要件を回避するコードは、絶対にコミットしないでください。
1. 認証・認可ミドルウェア
ユーザーデータを扱うすべての API ルートでは、適切なミドルウェアを使用する必要があります。
// ✅ CORRECT: Use withEmailAccount for email-scoped operations
export const GET = withEmailAccount(async (request, { params }) => {
const { emailAccountId } = request.auth;
// ...
});
// ✅ CORRECT: Use withAuth for user-scoped operations
export const GET = withAuth(async (request) => {
const { userId } = request.auth;
// ...
});
// ❌ WRONG: Direct access without authentication
export const GET = async (request) => {
// This exposes data to unauthenticated users!
const data = await prisma.user.findMany();
return NextResponse.json(data);
};
2. データアクセス制御
すべてのデータベースクエリは、認証済みのユーザー/アカウントにスコープを限定する必要があります。
// ✅ CORRECT: Always include user/account filtering
const schedule = await prisma.schedule.findUnique({
where: {
id: scheduleId,
emailAccountId // 🔒 Critical: Ensures user owns this resource
},
});
// ✅ CORRECT: Filter by user ownership
const rules = await prisma.rule.findMany({
where: {
emailAccountId, // 🔒 Only user's rules
enabled: true
},
});
// ❌ WRONG: Missing user/account filtering
const schedule = await prisma.schedule.findUnique({
where: { id: scheduleId }, // 🚨 Any user can access any schedule!
});
3. リソース所有権の検証
リソースが認証済みのユーザーに属していることを、必ず検証してください。
// ✅ CORRECT: Validate ownership before operations
async function updateRule({ ruleId, emailAccountId, data }) {
const rule = await prisma.rule.findUnique({
where: {
id: ruleId,
emailAccount: { id: emailAccountId } // 🔒 Ownership check
},
});
if (!rule) throw new SafeError("Rule not found"); // Returns 404, doesn't leak existence
return prisma.rule.update({
where: { id: ruleId },
data,
});
}
// ❌ WRONG: Direct updates without ownership validation
async function updateRule({ ruleId, data }) {
return prisma.rule.update({
where: { id: ruleId }, // 🚨 User can modify any rule!
data,
});
}
ミドルウェアの使用ガイドライン
withEmailAccount を使用する場合
特定のメールアカウントにスコープを限定する操作で使用します。
- メール、ルール、スケジュールなどの読み取り/書き込み
emailAccountIdを使用するあらゆる操作
export const GET = withEmailAccount(async (request) => {
const { emailAccountId, userId, email } = request.auth;
// All three fields available
});
withAuth を使用する場合
ユーザーレベルの操作で使用します。
- ユーザー設定、API キー、紹介
userIdのみを使用する操作
export const GET = withAuth(async (request) => {
const { userId } = request.auth;
// Only userId available
});
withError のみを使用する場合
公開エンドポイントや独自の認証を使用する場合に使います。
- 公開 Webhook(別途検証を行うもの)
- 独自の認証ロジックを持つエンドポイント
- Cron エンドポイント(
hasCronSecretの使用が必須です)
// ✅ CORRECT: Public endpoint with custom auth
export const GET = withError(async (request) => {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
});
// ✅ CORRECT: Cron endpoint with secret validation
export const POST = withError(async (request) => {
if (!hasCronSecret(request)) {
captureException(new Error("Unauthorized cron request"));
return new Response("Unauthorized", { status: 401 });
}
// ... cron logic
});
// ❌ WRONG: Cron endpoint without validation
export const POST = withError(async (request) => {
// 🚨 Anyone can trigger this cron job!
await sendDigestEmails();
});
Cron エンドポイントのセキュリティ
🚨 重要:適切な認証のない Cron エンドポイントは、誰でも起動できます。
Cron 認証パターン
// ✅ CORRECT: GET cron endpoint
export const GET = withError(async (request) => {
if (!hasCronSecret(request)) {
captureException(new Error("Unauthorized cron request"));
return new Response("Unauthorized", { status: 401 });
}
// Safe to execute cron logic
await processScheduledTasks();
return NextResponse.json({ success: true });
});
// ✅ CORRECT: POST cron endpoint
export const POST = withError(async (request) => {
if (!(await hasPostCronSecret(request))) {
captureException(new Error("Unauthorized cron request"));
return new Response("Unauthorized", { status: 401 });
}
// Safe to execute cron logic
await processBulkOperations();
return NextResponse.json({ success: true });
});
Cron セキュリティチェックリスト
自動タスクを実行するエンドポイントでは、次の点を確認してください。
-
withErrorミドルウェアを使用している(withAuthまたはwithEmailAccountではない) -
hasCronSecret(request)またはhasPostCronSecret(request)を使用して Cron シークレットを検証している -
captureExceptionを使用して不正なアクセス試行を記録している - 不正なリクエストに対して
401ステータスを返す - 一括操作、スケジュールされたタスク、またはシステムメンテナンスを含んでいる
一般的な Cron エンドポイントのパターン
// Digest/summary emails
export const POST = withError(async (request) => {
if (!hasCronSecret(request)) {
captureException(new Error("Unauthorized cron request: digest"));
return new Response("Unauthorized", { status: 401 });
}
await sendDigestEmails();
});
// Cleanup operations
export const POST = withError(async (request) => {
if (!(await hasPostCronSecret(request))) {
captureException(new Error("Unauthorized cron request: cleanup"));
return new Response("Unauthorized", { status: 401 });
}
await cleanupExpiredData();
});
// System monitoring
export const GET = withError(async (request) => {
if (!hasCronSecret(request)) {
captureException(new Error("Unauthorized cron request: monitor"));
return new Response("Unauthorized", { status: 401 });
}
await monitorSystemHealth();
});
環境の設定
CRON_SECRET が適切に設定されていることを確認してください。
# .env.local
CRON_SECRET=your-secure-random-secret-here
⚠️ 次のような推測しやすい Cron シークレットは絶対に使用しないでください。
"secret""password""cron"- 短い文字列や単純な文字列
データベースセキュリティのパターン
✅ 安全なクエリパターン
// User-scoped queries
const user = await prisma.user.findUnique({
where: { id: userId },
select: { id: true, email: true } // Only return needed fields
});
// Email account-scoped queries
const emailAccount = await prisma.emailAccount.findUnique({
where: { id: emailAccountId, userId }, // Double validation
});
// Related resource queries with ownership
const rule = await prisma.rule.findUnique({
where: {
id: ruleId,
emailAccount: { id: emailAccountId }
},
include: { actions: true }
});
// Filtered list queries
const schedules = await prisma.schedule.findMany({
where: { emailAccountId },
orderBy: { createdAt: 'desc' }
});
❌ 安全でないクエリパターン
// Missing user scoping
const schedules = await prisma.schedule.findMany(); // 🚨 Returns ALL schedules
// Missing ownership validation
const rule = await prisma.rule.findUnique({
where: { id: ruleId } // 🚨 Can access any user's rule
});
// Exposed sensitive fields
const user = await prisma.user.findUnique({
where: { id: userId }
// 🚨 Returns ALL fields including sensitive data
});
// Direct parameter usage
const userId = request.nextUrl.searchParams.get('userId');
const user = await prisma.user.findUnique({
where: { id: userId } // 🚨 User can access any user by changing URL
});
入力の検証とサニタイズ
パラメーターの検証
// ✅ CORRECT: Validate all inputs
export const GET = withEmailAccount(async (request, { params }) => {
const { id } = await params;
if (!id) {
return NextResponse.json(
{ error: "Missing schedule ID" },
{ status: 400 }
);
}
// Additional validation
if (typeof id !== 'string' || id.length < 10) {
return NextResponse.json(
{ error: "Invalid schedule ID format" },
{ status: 400 }
);
}
});
// ❌ WRONG: Using parameters without validation
export const GET = withEmailAccount(async (request, { params }) => {
const { id } = await params;
// 🚨 Direct usage without validation
const schedule = await prisma.schedule.findUnique({ where: { id } });
});
Zod によるボディの検証
// ✅ CORRECT: Always validate request bodies
const updateRuleSchema = z.object({
name: z.string().min(1).max(100),
enabled: z.boolean(),
conditions: z.array(z.object({
type: z.enum(['FROM', 'SUBJECT', 'BODY']),
value: z.string().min(1)
}))
});
export const PUT = withEmailAccount(async (request) => {
const body = await request.json();
const validatedData = updateRuleSchema.parse(body); // Throws on invalid data
// Use validatedData, not body
});
エラーハンドリングのセキュリティ
情報漏えいの防止
// ✅ CORRECT: Safe error responses
if (!rule) {
throw new SafeError("Rule not found"); // Generic 404
}
if (!hasPermission) {
throw new SafeError("Access denied"); // Generic 403
}
// ❌ WRONG: Information disclosure
if (!rule) {
throw new Error(`Rule ${ruleId} does not exist for user ${userId}`);
// 🚨 Reveals internal IDs and logic
}
if (!rule.emailAccountId === emailAccountId) {
throw new Error("This rule belongs to a different account");
// 🚨 Confirms existence of rule and reveals ownership info
}
一貫性のあるエラーレスポンス
// ✅ CORRECT: Consistent error format
export const GET = withEmailAccount(async (request) => {
try {
// ... operation
} catch (error) {
if (error instanceof SafeError) {
return NextResponse.json(
{ error: error.message, isKnownError: true },
{ status: error.statusCode || 400 }
);
}
// Let middleware handle unexpected errors
throw error;
}
});
よくあるセキュリティ脆弱性
1. 安全でない直接オブジェクト参照(IDOR)
// ❌ VULNERABLE: User can access any rule by changing ID
export const GET = async (request, { params }) => {
const { ruleId } = await params;
const rule = await prisma.rule.findUnique({ where: { id: ruleId } });
return NextResponse.json(rule);
};
// ✅ SECURE: Always validate ownership
export const GET = withEmailAccount(async (request, { params }) => {
const { emailAccountId } = request.auth;
const { ruleId } = await params;
const rule = await prisma.rule.findUnique({
where: {
id: ruleId,
emailAccount: { id: emailAccountId } // 🔒 Ownership validation
}
});
if (!rule) throw new SafeError("Rule not found");
return NextResponse.json(rule);
});
2. マスアサインメント
// ❌ VULNERABLE: User can modify any field
export const PUT = withEmailAccount(async (request) => {
const body = await request.json();
const rule = await prisma.rule.update({
where: { id: body.id },
data: body // 🚨 User controls all fields, including ownership!
});
});
// ✅ SECURE: Explicitly allow only safe fields
const updateSchema = z.object({
name: z.string(),
enabled: z.boolean(),
// Only allow specific fields
});
export const PUT = withEmailAccount(async (request) => {
const body = await request.json();
const validatedData = updateSchema.parse(body);
const rule = await prisma.rule.update({
where: {
id: ruleId,
emailAccount: { id: emailAccountId } // Maintain ownership
},
data: validatedData // Only validated fields
});
});
3. 権限昇格
// ❌ VULNERABLE: User can modify admin-only fields
const rule = await prisma.rule.update({
where: { id: ruleId },
data: {
...updateData,
// 🚨 What if updateData contains system fields?
ownerId: 'different-user-id', // User changes ownership!
systemGenerated: false, // User modifies system flags!
}
});
// ✅ SECURE: Whitelist approach
const allowedFields = {
name: updateData.name,
enabled: updateData.enabled,
instructions: updateData.instructions,
// Only explicitly allowed fields
};
const rule = await prisma.rule.update({
where: {
id: ruleId,
emailAccount: { id: emailAccountId }
},
data: allowedFields
});
4. 保護されていない Cron エンドポイント
// ❌ VULNERABLE: Anyone can trigger cron operations
export const POST = withError(async (request) => {
// 🚨 No authentication - anyone can send digest emails!
await sendDigestEmailsToAllUsers();
return NextResponse.json({ success: true });
});
// ❌ VULNERABLE: Weak cron validation
export const POST = withError(async (request) => {
const body = await request.json();
if (body.secret !== "simple-password") { // 🚨 Predictable secret
return new Response("Unauthorized", { status: 401 });
}
await performSystemMaintenance();
});
// ✅ SECURE: Proper cron authentication
export const POST = withError(async (request) => {
if (!hasCronSecret(request)) { // 🔒 Strong secret validation
captureException(new Error("Unauthorized cron request"));
return new Response("Unauthorized", { status: 401 });
}
await performSystemMaintenance();
});
API ルートのセキュリティチェックリスト
API ルートをデプロイする前に、次の点を確認してください。
認証 ✅
- 適切なミドルウェア(
withAuthまたはwithEmailAccount)を使用している - または、適切な検証を伴う
withErrorを使用している(Cron エンドポイント、Webhook) - Cron エンドポイントで
hasCronSecret()またはhasPostCronSecret()を使用している - ユーザーデータへの公開アクセスがない
- セッション/トークンの検証を必須にしている
認可 ✅
- すべてのクエリにユーザー/アカウントのフィルターを含めている
- 操作の前にリソースの所有権を検証している
- 所有権チェックなしで直接オブジェクト参照を行っていない
入力の検証 ✅
- すべてのパラメーターを検証している(型、形式、長さ)
- リクエストボディに Zod スキーマを使用している
- SQL インジェクションを防止している(Prisma を正しく使用)
- クエリにユーザー入力を直接使用していない
データ保護 ✅
- 必要なフィールドのみを返している
- レスポンスで機密データを公開していない
- エラーメッセージから情報が漏えいしない
- 一貫したエラーレスポンス形式を使用している
クエリのセキュリティ ✅
- すべての
findUnique/findFirst呼び出しに所有権フィルターを含めている - すべての
findMany呼び出しをユーザーデータにスコープ限定している - 他のユーザーのデータを返すクエリがない
- アクセス制御に Prisma のリレーションを適切に使用している
コードベースの例
✅ 良い例
Frequency API - apps/web/app/api/user/frequency/[id]/route.ts
export const GET = withEmailAccount(async (request, { params }) => {
const emailAccountId = request.auth.emailAccountId;
const { id } = await params;
if (!id) return NextResponse.json({ error: "Missing frequency id" }, { status: 400 });
const schedule = await prisma.schedule.findUnique({
where: { id, emailAccountId }, // 🔒 Scoped to user's account
});
if (!schedule) {
return NextResponse.json({ error: "Schedule not found" }, { status: 404 });
}
return NextResponse.json(schedule);
});
Rules API - apps/web/app/api/user/rules/[id]/route.ts
const rule = await prisma.rule.findUnique({
where: {
id: ruleId,
emailAccount: { id: emailAccountId } // 🔒 Relationship-based ownership check
},
include: { actions: true, categoryFilters: true },
});
セキュリティレビューの対象領域
コードをレビューする際は、特に次の点に注意してください。
- 新しい API ルート - ミドルウェアが適切に使用されていることを確認します
- データベースクエリ - ユーザーのスコープが適切に設定されていることを確認します
- パラメーターの処理 - 検証とサニタイズを確認します
- エラーレスポンス - 情報漏えいがないことを確認します
- 一括操作 - 大量の更新や削除には特に注意します
セキュリティテスト
手動テストのチェックリスト
- 認証のバイパス:認証ヘッダーなしでエンドポイントへのアクセスを試します
- IDOR テスト:リソース ID を変更して、他のユーザーのデータにアクセスできるか試します
- パラメーターの改ざん:無効なパラメーターや悪意のあるパラメーターでテストします
- エラー情報:エラーによって機密情報が漏えいしないか確認します
自動セキュリティテスト
テストスイートにセキュリティテストを含めてください。
describe("Security Tests", () => {
it("should not allow access without authentication", async () => {
const response = await request.get("/api/user/rules/123");
expect(response.status).toBe(401);
});
it("should not allow access to other users' resources", async () => {
const response = await request
.get("/api/user/rules/other-user-rule-id")
.set("Authorization", "Bearer valid-token")
.set("X-Email-Account-ID", "user-account-id");
expect(response.status).toBe(404); // Not 403, to avoid info disclosure
});
});
デプロイのセキュリティ
環境変数
- すべての機密データを環境変数に格納する
- コードやバージョン管理にシークレットを含めない
- 環境ごとに異なるシークレットを使用する
監視とロギング
- セキュリティイベントを記録する
- 認証失敗の試行を追跡する
- 通常とは異なるアクセスパターンを監視する
- ログに機密データを含めない
セキュリティは任意ではありません。ユーザーデータを扱うすべての API ルートは、これらのガイドラインに従う必要があります。判断に迷った場合は、慎重な側に倒し、セキュリティチェックを追加してください。