mcpField
S
security

security

github.com/elie222セキュリティ12.8k36 回インストール今日更新
リポジトリ

security について

Inbox Zero のコードレビュー向けセキュリティガイドスキルです。ルート認証、ユーザー単位のデータアクセス、入力検証、安全なエラー処理、cron 保護を扱います。

主な機能

  1. 01認証ミドルウェアの選択
  2. 02ユーザー・アカウント単位のデータ制限
  3. 03リソース所有権の確認
  4. 04入力検証とサニタイズ
  5. 05安全なエラー処理
  6. 06cron エンドポイントの認証

活用シーン

  1. 01API ルートの認証方式のレビュー
  2. 02データベースクエリのユーザー制限確認
  3. 03IDOR を想定した所有権保護のテスト
  4. 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 },
});
セキュリティレビューの対象領域

コードをレビューする際は、特に次の点に注意してください。

  1. 新しい API ルート - ミドルウェアが適切に使用されていることを確認します
  2. データベースクエリ - ユーザーのスコープが適切に設定されていることを確認します
  3. パラメーターの処理 - 検証とサニタイズを確認します
  4. エラーレスポンス - 情報漏えいがないことを確認します
  5. 一括操作 - 大量の更新や削除には特に注意します

セキュリティテスト

手動テストのチェックリスト
  1. 認証のバイパス:認証ヘッダーなしでエンドポイントへのアクセスを試します
  2. IDOR テスト:リソース ID を変更して、他のユーザーのデータにアクセスできるか試します
  3. パラメーターの改ざん:無効なパラメーターや悪意のあるパラメーターでテストします
  4. エラー情報:エラーによって機密情報が漏えいしないか確認します
自動セキュリティテスト

テストスイートにセキュリティテストを含めてください。

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 ルートは、これらのガイドラインに従う必要があります。判断に迷った場合は、慎重な側に倒し、セキュリティチェックを追加してください。

コメント