security
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
withErrormiddleware (notwithAuthorwithEmailAccount) - Validates cron secret using
hasCronSecret(request)orhasPostCronSecret(request) - Captures unauthorized attempts with
captureException - Returns
401status 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 (
withAuthorwithEmailAccount) - Or uses
withErrorwith proper validation (cron endpoints, webhooks) - Cron endpoints use
hasCronSecret()orhasPostCronSecret() - 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/findFirstcalls include ownership filters - All
findManycalls 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:
- New API routes - Ensure proper middleware usage
- Database queries - Verify user scoping
- Parameter handling - Check validation and sanitization
- Error responses - Ensure no information disclosure
- Bulk operations - Extra care for mass updates/deletes
Security Testing
Manual Testing Checklist
- Authentication bypass: Try accessing endpoints without auth headers
- IDOR testing: Modify resource IDs to access other users' data
- Parameter manipulation: Test with invalid/malicious parameters
- 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.