elie222

security

Security guidelines for API route development

elie222 11,092 1,363 Updated 3mo ago
GitHub

Install

npx skillscat add elie222/inbox-zero/security

Install via the SkillsCat registry.

SKILL.md

Security Guidelines

Critical Security Rules

๐Ÿšจ NEVER commit code that bypasses these security requirements.

1. Authentication & Authorization Middleware

ALL API routes that handle user data MUST use appropriate middleware:

// โœ… 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. Data Access Control

ALL database queries MUST be scoped to the authenticated user/account:

// โœ… 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. Resource Ownership Validation

Always validate that resources belong to the authenticated user:

// โœ… 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,
  });
}

Middleware Usage Guidelines

When to use withEmailAccount

Use for operations that are scoped to a specific email account:

  • Reading/writing emails, rules, schedules, etc.
  • Any operation that uses emailAccountId
export const GET = withEmailAccount(async (request) => {
  const { emailAccountId, userId, email } = request.auth;
  // All three fields available
});

When to use withAuth

Use for user-level operations:

  • User settings, API keys, referrals
  • Operations that use only userId
export const GET = withAuth(async (request) => {
  const { userId } = request.auth;
  // Only userId available
});

When to use withError only

Use for public endpoints or custom authentication:

  • Public webhooks (with separate validation)
  • Endpoints with custom auth logic
  • Cron endpoints (MUST use 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 Endpoint Security

๐Ÿšจ CRITICAL: Cron endpoints without proper authentication can be triggered by anyone!

Cron Authentication Patterns

// โœ… 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 Security Checklist

For any endpoint that performs automated tasks:

  • Uses withError middleware (not withAuth or withEmailAccount)
  • Validates cron secret using hasCronSecret(request) or hasPostCronSecret(request)
  • Captures unauthorized attempts with captureException
  • Returns 401 status for unauthorized requests
  • Contains bulk operations, scheduled tasks, or system maintenance

Common Cron Endpoint Patterns

// 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();
});

Environment Setup

Ensure CRON_SECRET is properly configured:

# .env.local
CRON_SECRET=your-secure-random-secret-here

โš ๏ธ Never use predictable cron secrets like:

  • "secret"
  • "password"
  • "cron"
  • Short or simple strings

Database Security Patterns

โœ… Secure Query Patterns

// 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' }
});

โŒ Insecure Query Patterns

// 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
});

Input Validation & Sanitization

Parameter Validation

// โœ… 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 } });
});

Body Validation with 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
});

Error Handling Security

Information Disclosure Prevention

// โœ… 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
}

Consistent Error Responses

// โœ… 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;
  }
});

Common Security Vulnerabilities

1. Insecure Direct Object References (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. Mass Assignment

// โŒ 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. Privilege Escalation

// โŒ 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. Unprotected Cron Endpoints

// โŒ 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();
});

Security Checklist for API Routes

Before deploying any API route, verify:

Authentication โœ…

  • Uses appropriate middleware (withAuth or withEmailAccount)
  • Or uses withError with proper validation (cron endpoints, webhooks)
  • Cron endpoints use hasCronSecret() or hasPostCronSecret()
  • No public access to user data
  • Session/token validation is enforced

Authorization โœ…

  • All queries include user/account filtering
  • Resource ownership is validated before operations
  • No direct object references without ownership checks

Input Validation โœ…

  • All parameters are validated (type, format, length)
  • Request bodies use Zod schemas
  • SQL injection prevention (using Prisma correctly)
  • No user input directly in queries

Data Protection โœ…

  • Only necessary fields are returned
  • Sensitive data is not exposed in responses
  • Error messages don't leak information
  • Consistent error response format

Query Security โœ…

  • All findUnique/findFirst calls include ownership filters
  • All findMany calls are scoped to user's data
  • No queries return data from other users
  • Proper use of Prisma relationships for access control

Examples from Codebase

โœ… Good Examples

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 },
});

Areas for Security Review

When reviewing code, pay special attention to:

  1. New API routes - Ensure proper middleware usage
  2. Database queries - Verify user scoping
  3. Parameter handling - Check validation and sanitization
  4. Error responses - Ensure no information disclosure
  5. Bulk operations - Extra care for mass updates/deletes

Security Testing

Manual Testing Checklist

  1. Authentication bypass: Try accessing endpoints without auth headers
  2. IDOR testing: Modify resource IDs to access other users' data
  3. Parameter manipulation: Test with invalid/malicious parameters
  4. Error information: Check if errors reveal sensitive information

Automated Security Tests

Include security tests in your test suites:

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
  });
});

Deployment Security

Environment Variables

  • All sensitive data in environment variables
  • No secrets in code or version control
  • Different secrets for different environments

Monitoring & Logging

  • Security events are logged
  • Failed authentication attempts tracked
  • Unusual access patterns monitored
  • No sensitive data in logs

Remember: Security is not optional. Every API route that handles user data must follow these guidelines. When in doubt, err on the side of caution and add extra security checks.