Comprehensive security implementation guide for Dirt Free CRM.
- Overview
- Security Layers
- Webhook Signature Verification
- Request Throttling
- Input Validation & Sanitization
- CSRF Protection
- Security Headers
- SQL Injection Prevention
- Best Practices
- Security Checklist
The security system implements multiple layers of protection:
┌─────────────────────────────────────────────────────────┐
│ Request Received │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 1: Security Headers │
│ (Middleware - All Requests) │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 2: Authentication & Authorization │
│ (withAuth middleware) │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 3: CSRF Protection │
│ (State-changing requests only) │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 4: Rate Limiting │
│ (Per-user, per-action throttling) │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 5: Input Validation & Sanitization │
│ (Zod schemas) │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 6: Business Logic │
│ (Parameterized queries, audit logging) │
└─────────────────────────────────────────────────────────┘
File: /src/middleware.ts
Automatically applied to all requests:
| Header | Purpose | Value |
|---|---|---|
X-Content-Type-Options |
Prevent MIME sniffing | nosniff |
X-Frame-Options |
Prevent clickjacking | DENY |
X-XSS-Protection |
Enable XSS filter | 1; mode=block |
Referrer-Policy |
Control referrer info | strict-origin-when-cross-origin |
Permissions-Policy |
Disable unused APIs | geolocation=(), microphone=()... |
Content-Security-Policy |
Prevent XSS, injection | See CSP section |
Strict-Transport-Security |
Force HTTPS (prod only) | max-age=31536000 |
File: /src/middleware/api-auth.ts
See Authentication Guide for details.
File: /src/lib/security/csrf.ts
Prevents cross-site request forgery attacks.
File: /src/lib/security/throttling.ts
Prevents abuse through request throttling.
File: /src/lib/security/sanitize.ts
Validates and sanitizes all user input.
File: /src/lib/security/signature-verification.ts
Verifies webhook authenticity.
Webhooks can be spoofed by attackers. Signature verification ensures:
- Requests actually come from the expected service
- Payload hasn't been tampered with
- Protection against replay attacks (with timestamps)
import { withWebhookVerification } from '@/lib/security/signature-verification'
export const POST = withWebhookVerification(
async (req, payload) => {
// Payload is verified and safe to use
const data = JSON.parse(payload)
// Process webhook...
return NextResponse.json({ success: true })
},
{
secret: process.env.WEBHOOK_SECRET,
signatureHeader: 'x-webhook-signature'
}
)Prevent replay attacks by verifying request age:
export const POST = withWebhookVerification(
handler,
{
secret: process.env.WEBHOOK_SECRET,
signatureHeader: 'x-webhook-signature',
timestampHeader: 'x-webhook-timestamp',
timestampTolerance: 300 // 5 minutes
}
)Support multiple secrets for graceful rotation:
import { verifyWebhookSignatureWithRotation } from '@/lib/security/signature-verification'
const isValid = verifyWebhookSignatureWithRotation(
payload,
signature,
[
process.env.WEBHOOK_SECRET, // Current secret
process.env.WEBHOOK_SECRET_OLD // Old secret
]
)Generate signatures for outgoing webhooks:
import { generateWebhookSignature } from '@/lib/security/signature-verification'
const payload = JSON.stringify(data)
const signature = generateWebhookSignature(payload, secret)
await fetch(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature
},
body: payload
})Rate limiting prevents:
- Brute force attacks
- API abuse
- Resource exhaustion
- DDoS attacks
- Accidental infinite loops
| Action | Limit | Window | Use Case |
|---|---|---|---|
opportunities |
20 req | 60s | Opportunity operations |
promotions |
10 req | 60s | Promotion management |
analytics |
30 req | 60s | Analytics queries |
export |
5 req | 60s | Data exports |
auth |
5 req | 5 min | Login attempts |
passwordReset |
3 req | 1 hour | Password resets |
sms |
10 req | 1 hour | SMS sending |
email |
20 req | 1 hour | Email sending |
import { checkThrottle } from '@/lib/security/throttling'
export const POST = withAuth(async (req, { user }) => {
// Check throttle
const result = await checkThrottle(user.id, 'opportunities')
if (!result.allowed) {
return NextResponse.json(
{
error: 'Rate limit exceeded',
retryAfter: result.resetAfter
},
{
status: 429,
headers: {
'Retry-After': result.resetAfter.toString()
}
}
)
}
// Process request...
})import { withThrottling } from '@/lib/security/throttling'
export const POST = withThrottling(
async (req, context) => {
// Request is not throttled
return NextResponse.json({ success: true })
},
'opportunities',
(req, context) => context.user.id
)import { createThrottleConfig, checkThrottle } from '@/lib/security/throttling'
const customConfig = createThrottleConfig(
100, // 100 requests
60, // per 60 seconds
'Custom rate limit exceeded'
)
const result = await checkThrottle(userId, 'custom-action', customConfig)For pre-authentication endpoints:
import { checkThrottleByIP } from '@/lib/security/throttling'
export async function POST(req: Request) {
const ip = req.headers.get('x-forwarded-for') || 'unknown'
const result = await checkThrottleByIP(ip, 'auth')
if (!result.allowed) {
return NextResponse.json({ error: 'Too many requests' }, { status: 429 })
}
// Process login...
}Prevents:
- SQL injection
- XSS attacks
- Data corruption
- Buffer overflow
- Path traversal
import { schemas } from '@/lib/security/sanitize'
// Available schemas:
schemas.email // Email address
schemas.phone // E.164 phone number
schemas.phoneUS // US phone number
schemas.currency // Money amount
schemas.percentage // 0-100 percentage
schemas.text // Generic text (max 10K chars)
schemas.shortText // Short text (max 255 chars)
schemas.longText // Long text (max 50K chars)
schemas.id // UUID
schemas.url // URL
schemas.date // ISO 8601 date
schemas.postalCode // Postal code
schemas.stateUS // US state code
schemas.alphanumeric // Letters and numbers only
schemas.slug // URL-friendly slug
schemas.hexColor // Hex color code
schemas.integer // Integer
schemas.positiveInteger // Positive integer
schemas.latitude // Latitude (-90 to 90)
schemas.longitude // Longitude (-180 to 180)import { createValidationMiddleware, schemas } from '@/lib/security/sanitize'
import { z } from 'zod'
const schema = z.object({
email: schemas.email,
name: schemas.shortText,
phone: schemas.phone.optional()
})
const validate = createValidationMiddleware(schema)
export const POST = validate(async (req, { data }) => {
// data is fully validated and typed
const { email, name, phone } = data
return NextResponse.json({ success: true })
})import { validateRequestBody, schemas } from '@/lib/security/sanitize'
import { z } from 'zod'
export async function POST(req: Request) {
const body = await req.json()
const schema = z.object({
email: schemas.email,
amount: schemas.currency
})
const result = validateRequestBody(body, schema)
if (!result.success) {
return NextResponse.json(
{ errors: result.errors.format() },
{ status: 400 }
)
}
const { email, amount } = result.data
// Use validated data...
}import {
sanitizeHtml,
sanitizeSql,
stripHtmlTags,
sanitizeFilename,
normalizeWhitespace
} from '@/lib/security/sanitize'
// Escape HTML entities
const safe = sanitizeHtml('<script>alert("xss")</script>')
// Result: '<script>alert("xss")</script>'
// Remove HTML tags
const text = stripHtmlTags('<p>Hello <b>World</b></p>')
// Result: 'Hello World'
// Sanitize filename (prevent path traversal)
const filename = sanitizeFilename('../../../etc/passwd')
// Result: 'etcpasswd'
// Normalize whitespace
const normalized = normalizeWhitespace(' Hello World \n\n')
// Result: 'Hello World'Pre-built schemas for common entities:
import { entitySchemas } from '@/lib/security/sanitize'
// Customer schema
entitySchemas.customer
// Job schema
entitySchemas.job
// Opportunity schema
entitySchemas.opportunity
// Promotion schema
entitySchemas.promotionPrevents attackers from:
- Making unauthorized requests on behalf of authenticated users
- Changing passwords
- Making purchases
- Deleting data
- Any state-changing operation
Client-side:
// Fetch CSRF token
const response = await fetch('/api/csrf-token')
const { csrfToken } = await response.json()
// Store in memory or state (never localStorage)
setCsrfToken(csrfToken)Include in request headers:
await fetch('/api/protected-endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
})import { withCsrfProtection } from '@/lib/security/csrf'
export const POST = withCsrfProtection(
async (req) => {
// Request has valid CSRF token
return NextResponse.json({ success: true })
},
{
tokenHeader: 'x-csrf-token',
skipSafeMethods: true // Skip GET, HEAD, OPTIONS
}
)Alternative to token storage:
import { withDoubleSubmitCookie } from '@/lib/security/csrf'
export const POST = withDoubleSubmitCookie(async (req) => {
// Verified via cookie and header match
return NextResponse.json({ success: true })
})Configured in /src/middleware.ts:
const csp = `
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https: blob:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' ${process.env.NEXT_PUBLIC_SUPABASE_URL};
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
`Update /src/middleware.ts to adjust CSP based on your needs:
// Allow specific domains
connect-src 'self' https://api.stripe.com https://*.supabase.co;
// Allow inline scripts with nonce
script-src 'self' 'nonce-{random}';
// Report CSP violations
report-uri /api/csp-report;Automatically enabled in production:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload✅ GOOD - Supabase uses parameterized queries:
const { data } = await supabase
.from('customers')
.select('*')
.eq('email', userInput) // Safe - parameterized❌ BAD - Never construct raw SQL with user input:
// NEVER DO THIS!
const { data } = await supabase.rpc('raw_query', {
query: `SELECT * FROM customers WHERE email = '${userInput}'`
})If you must use RPC, use parameters:
-- In database
CREATE FUNCTION search_customers(search_term TEXT)
RETURNS TABLE (...) AS $$
SELECT * FROM customers
WHERE name ILIKE '%' || search_term || '%'
$$ LANGUAGE sql;// In code
const { data } = await supabase
.rpc('search_customers', {
search_term: userInput // Safe - parameterized
})Even with parameterized queries, sanitize input:
import { sanitizeSql } from '@/lib/security/sanitize'
const cleaned = sanitizeSql(userInput)
// Removes quotes, semicolons, backslashesApply multiple security layers:
export const POST = withAuth( // Layer 1: Auth
async (req, { user }) => {
// Layer 2: CSRF
const csrfToken = req.headers.get('x-csrf-token')
if (!verifyCsrfToken(sessionId, csrfToken)) {
return error403()
}
// Layer 3: Throttle
const throttle = await checkThrottle(user.id, 'action')
if (!throttle.allowed) {
return error429()
}
// Layer 4: Validate
const result = validateRequestBody(await req.json(), schema)
if (!result.success) {
return error400()
}
// Process with validated data
return success()
},
{ requirePermission: 'permission', enableAuditLog: true }
)Only grant minimum necessary permissions:
// Bad - too permissive
{ requirePermission: 'system:admin' }
// Good - specific permission
{ requirePermission: 'customers:write' }Validate ALL user input:
// Validate
const schema = z.object({
email: schemas.email,
amount: schemas.currency.max(10000)
})
// Sanitize
const clean = sanitizeHtml(userInput)
// Both
const result = validateRequestBody(body, schema)Escape data before rendering:
import { sanitizeHtml } from '@/lib/security/sanitize'
// In component
<div>{sanitizeHtml(userContent)}</div>Never store secrets in code or client-side:
// Bad
const API_KEY = 'sk_live_abc123'
// Good
const API_KEY = process.env.API_SECRET_KEYDon't leak sensitive info in errors:
// Bad
catch (error) {
return NextResponse.json({ error: error.message })
}
// Good
catch (error) {
console.error('Database error:', error)
return NextResponse.json({ error: 'Internal server error' })
}Log sensitive operations:
export const DELETE = withAuth(
handler,
{
requirePermission: 'customers:delete',
enableAuditLog: true // Log all deletions
}
)Keep dependencies updated:
npm audit
npm update
npm audit fix- Authentication required (
withAuth) - Permission check (
requirePermission) - Rate limiting (
checkThrottle) - Input validation (Zod schema)
- Input sanitization
- CSRF protection (state-changing)
- Audit logging (sensitive ops)
- Error handling (no leaks)
- Force dynamic (
export const dynamic = 'force-dynamic')
- Signature verification
- Timestamp verification
- Secret rotation support
- Replay attack prevention
- Output encoding
- CSRF token handling
- Secure storage (no localStorage for tokens)
- Permission guards
- Input validation
- Error handling
- Parameterized queries only
- RLS policies enabled
- Audit logging
- Regular backups
- Encrypted connections
- HTTPS enabled
- Security headers configured
- Environment variables secured
- Secrets rotation
- Monitoring enabled
- Regular security audits
/src/lib/security/signature-verification.ts- Webhook security/src/lib/security/throttling.ts- Rate limiting/src/lib/security/sanitize.ts- Input validation/src/lib/security/csrf.ts- CSRF protection/src/middleware.ts- Security headers/src/middleware/api-auth.ts- Authentication/src/app/api/examples/security/- Examples
Last Updated: 2025-10-24