Security Patterns
Security patterns for handling sensitive data, preventing information leakage, and protecting API boundaries.
Contents
- •Redact Sensitive Data from Logs
- •Validate Input with Zod at API Boundaries
- •Prevent Error Information Leakage
- •Never Expose Tokens in URLs or Client Code
- •Verify Webhook Signatures Before Processing
- •Handle Payment Security Edge Cases
Redact Sensitive Data from Logs
Never log any of the following:
- •Credit card numbers, CVV codes, full account numbers
- •Authentication tokens (
token,customerToken) - •Session secrets
- •Passwords or security answers
- •IP addresses (for privacy)
When logging API responses or request payloads, strip sensitive fields before passing to logger:
import { logger } from '@/lib/logger'
// GOOD: Log only safe metadata
logger.info('[Checkout] Payment processed', {
tags: { source: 'payments-api', operation: 'create-order' },
metadata: { orderId: order.id, amount: order.total },
})
// BAD: Logging full API response that may contain card data
logger.info('[Checkout] Payment response', { response: fullApiResponse })
Why: PCI DSS requires that cardholder data never appears in logs. DataDog indexes log content, making leaked data searchable and persistent.
Validate Input with Zod at API Boundaries
Use Zod validation at every boundary where untrusted data enters the system: API routes, webhook handlers, and external API responses.
// API route — validate incoming request body
const CreateOrderSchema = z.object({
items: z.array(z.object({ sku: z.string(), quantity: z.number().positive() })),
shippingAddressId: z.string(),
})
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const parsed = CreateOrderSchema.safeParse(req.body)
if (!parsed.success) {
return res.status(400).json(createErrorResponse({
code: 'VALIDATION_ERROR',
message: 'Invalid request body',
}))
}
// Use parsed.data — fully typed and validated
}
Validate at boundaries, trust internally:
- •API route handlers — validate
req.body,req.query - •Webhook receivers — validate payload after signature check
- •External API responses — validate response shape before using
- •Don't re-validate data passed between internal functions
Why: Zod at I/O boundaries catches malformed data before it enters the system. Internal code can trust validated types.
Prevent Error Information Leakage
Never expose to users:
- •Stack traces
- •Database queries or connection strings
- •Internal API error codes from external services
- •File paths or server configuration
- •Service versions
Use createErrorResponse from src/lib/api/create-error-response.ts for consistent, safe error responses:
// GOOD: Safe error response
return res.status(400).json(createErrorResponse({
code: 'CARD_DECLINED',
message: 'Your card was declined. Please try a different payment method.',
}))
// BAD: Leaking internals
return res.status(500).json({
error: `External API error: ${error.message}`,
stack: error.stack,
})
Log the full error server-side, return a safe message client-side:
logger.error('[Checkout] Order creation failed', {
tags: { source: 'payments-api', operation: 'create-order', status: 'error' },
metadata: { orderId, durationMs },
error,
})
return res.status(500).json(createErrorResponse({
code: 'ORDER_FAILED',
message: 'Unable to process your order. Please try again.',
}))
Why: Internal details help attackers map your system. User-facing error codes are sufficient for support tickets.
Never Expose Tokens in URLs or Client Code
// BAD: Token in URL query parameter
router.push(`/api/order?token=${customerToken}`)
// GOOD: Token in headers
fetch('/api/order', {
headers: { Authorization: `Bearer ${customerToken}` },
})
Token handling rules:
- •Store tokens in Zustand
userslice (memory only, not persisted) - •Send tokens via
Authorizationheader, never in URL params - •Never log tokens (see redaction rules above)
- •Server-only secrets use
NEXT_PRIVATE_env var prefix andimport 'server-only'
Why: URL parameters appear in browser history, server logs, and referrer headers. Header-based tokens avoid these leakage vectors.
Verify Webhook Signatures Before Processing
When receiving webhooks from external services, always verify the signature before processing the payload:
import { createHmac } from 'crypto'
function verifyWebhookSignature({
payload,
signature,
secret,
timestamp,
}: {
payload: string
signature: string
secret: string
timestamp: string
}): boolean {
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${payload}`)
.digest('hex')
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}
Always:
- •Verify signature before any processing
- •Use
timingSafeEqualto prevent timing attacks - •Check timestamp to prevent replay attacks (reject if > 5 minutes old)
- •Return 200 quickly, process asynchronously if needed
Why: Without signature verification, anyone can send fake webhook payloads to your endpoint.
Handle Payment Security Edge Cases
Idempotency
Use idempotency keys for payment operations to prevent duplicate charges:
- •Generate a unique key per user action (not per retry)
- •Store and reuse the key across retries of the same operation
- •Let the payment service handle deduplication
Race Conditions
- •Disable submit buttons while
mutation.isPending(derive from React Query, don't use separateuseState) - •Use optimistic locking with version checks for concurrent modifications
- •Database-level constraints as the final safety net
Payment Failures
- •Retry with exponential backoff for transient errors (network, timeouts)
- •Do NOT retry for validation errors (declined card, insufficient funds)
- •Clear, user-friendly error messages without exposing internal details
Why: Payment edge cases can result in double charges, lost transactions, or security vulnerabilities if not handled explicitly.