Security Best Practices
Comprehensive security guide for building secure SonicJS applications. Learn how to protect your API, users, and data.
Overview
SonicJS is built with security in mind, implementing industry best practices out of the box. This guide covers the security features available and how to configure them properly for production use.
JWT Authentication
Secure token-based authentication with automatic expiration
SQL Injection Protection
All queries use parameterized statements
Input Validation
Zod-based schema validation on all inputs
HTTPS Cookies
Secure, HttpOnly, SameSite cookies by default
Authentication
JWT Token Security
SonicJS uses JWT (JSON Web Tokens) for authentication with these security features:
- HS256 Algorithm: HMAC-SHA256 for token signing
- 24-hour Expiration: Tokens automatically expire
- Secure Storage: Tokens stored in HttpOnly cookies
Token Payload Structure
interface JWTPayload {
userId: string // User identifier
email: string // User email
role: string // User role for authorization
exp: number // Expiration timestamp
iat: number // Issued at timestamp
}
Cookie Security
Authentication cookies are configured with maximum security:
Cookie Configuration
// Default cookie settings (set automatically)
{
httpOnly: true, // Prevents JavaScript access
secure: true, // HTTPS only
sameSite: 'Strict', // Prevents CSRF
maxAge: 86400, // 24 hours
path: '/'
}
Password Security
Passwords are hashed using the Web Crypto API:
Password Hashing
// SonicJS hashes passwords automatically
// Using SHA-256 with salt
// Registration - password is hashed before storage
const hashedPassword = await hashPassword(plainPassword)
// Login - compare hashed values
const isValid = await verifyPassword(plainPassword, storedHash)
Always use a unique, strong JWT secret in production. Never use the default secret.
Multi-Factor Authentication
For enhanced security, use the OTP Login plugin:
Enable OTP
// OTP provides additional verification
// 1. User enters email
// 2. Receives one-time code
// 3. Enters code to complete login
// OTP security features:
// - Rate limiting per hour
// - Maximum attempt limits
// - Short expiration time
// - IP and user agent tracking
Authorization
Role-Based Access Control
SonicJS implements RBAC with four built-in roles:
| Role | Permissions |
|---|---|
admin | Full access to all features |
editor | Create, edit, publish content |
author | Create and edit own content |
viewer | Read-only access |
Protecting Routes
Route Protection
import { requireAuth, requireRole } from '@sonicjs-cms/core'
// Require authentication
app.get('/api/profile', requireAuth(), async (c) => {
const user = c.get('user')
return c.json(user)
})
// Require specific role
app.delete('/api/users/:id', requireRole('admin'), async (c) => {
// Only admins can delete users
})
// Require one of multiple roles
app.post('/api/content', requireRole(['admin', 'editor']), async (c) => {
// Admins and editors can create content
})
Permission Checking
Manual Permission Check
app.get('/api/content/:id', requireAuth(), async (c) => {
const user = c.get('user')
const content = await getContent(c.req.param('id'))
// Authors can only edit their own content
if (user.role === 'author' && content.authorId !== user.userId) {
return c.json({ error: 'Not authorized' }, 403)
}
return c.json(content)
})
Input Validation
Schema Validation with Zod
All user input should be validated before processing:
Request Validation
import { z } from 'zod'
// Define validation schema
const createPostSchema = z.object({
title: z.string()
.min(1, 'Title is required')
.max(200, 'Title too long'),
content: z.string()
.min(1, 'Content is required'),
slug: z.string()
.regex(/^[a-z0-9-]+$/, 'Invalid slug format')
.optional(),
publishedAt: z.string()
.datetime()
.optional()
})
// Use in route
app.post('/api/posts', requireAuth(), async (c) => {
const body = await c.req.json()
// Validate input
const result = createPostSchema.safeParse(body)
if (!result.success) {
return c.json({
error: 'Validation failed',
details: result.error.issues
}, 400)
}
// Use validated data
const post = await createPost(result.data)
return c.json(post, 201)
})
Email Validation
Email Handling
import { z } from 'zod'
const emailSchema = z.string()
.email('Valid email is required')
.transform(email => email.toLowerCase().trim())
// Always normalize emails
const normalizedEmail = emailSchema.parse(input.email)
File Upload Validation
Upload Validation
// Validate file uploads
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf']
const maxSize = 10 * 1024 * 1024 // 10MB
app.post('/api/upload', async (c) => {
const formData = await c.req.formData()
const file = formData.get('file') as File
// Check file type
if (!allowedTypes.includes(file.type)) {
return c.json({ error: 'Invalid file type' }, 400)
}
// Check file size
if (file.size > maxSize) {
return c.json({ error: 'File too large' }, 400)
}
// Process file...
})
SQL Injection Prevention
Parameterized Queries
SonicJS uses Cloudflare D1 with parameterized queries to prevent SQL injection:
Safe Query Pattern
// SAFE - Using parameterized queries
const user = await db
.prepare('SELECT * FROM users WHERE email = ?')
.bind(email)
.first()
// SAFE - Multiple parameters
const content = await db
.prepare('SELECT * FROM content WHERE collection_id = ? AND status = ?')
.bind(collectionId, status)
.all()
// DANGEROUS - Never do this!
// const user = await db.prepare(`SELECT * FROM users WHERE email = '${email}'`).first()
Query Filter Builder
Use the built-in QueryFilterBuilder for complex queries:
QueryFilterBuilder
import { QueryFilterBuilder } from '@sonicjs-cms/core'
// Builds safe parameterized queries
const builder = new QueryFilterBuilder()
builder
.where('status', '=', 'published')
.where('category', '=', categoryId)
.orderBy('created_at', 'desc')
.limit(10)
const { sql, params } = builder.build()
// sql: "WHERE status = ? AND category = ? ORDER BY created_at DESC LIMIT 10"
// params: ['published', categoryId]
const results = await db.prepare(`SELECT * FROM content ${sql}`).bind(...params).all()
Field Name Sanitization
Field names are automatically sanitized:
Field Sanitization
// QueryFilterBuilder sanitizes field names
// Only allows: a-z, A-Z, 0-9, _, ., $
// This prevents injection through field names:
// sanitize('user; DROP TABLE users--') => 'userDROPTABLEusers'
XSS Prevention
HTML Escaping
SonicJS provides utilities for escaping HTML output:
HTML Escaping
import { escapeHtml, sanitizeInput } from '@sonicjs-cms/core'
// Escape HTML entities
const safe = escapeHtml('<script>alert("xss")</script>')
// Result: <script>alert("xss")</script>
// Sanitize and trim input
const cleanInput = sanitizeInput(userInput)
// Sanitize specific object fields
const cleanUser = sanitizeObject(userData, ['name', 'bio'])
Characters Escaped
| Character | Entity |
|---|---|
& | & |
< | < |
> | > |
" | " |
' | ' |
Content Security
Safe Content Rendering
// When rendering user content
app.get('/api/preview/:id', async (c) => {
const content = await getContent(c.req.param('id'))
// Escape user-generated content before rendering
return c.html(`
<h1>${escapeHtml(content.title)}</h1>
<div>${content.sanitizedHtml}</div>
`)
})
CSRF Protection
SameSite Cookies
SonicJS uses SameSite: Strict cookies by default, which provides strong CSRF protection:
CSRF Protection
// Cookies are automatically set with:
// SameSite: 'Strict'
// This means:
// - Cookie only sent on same-origin requests
// - Not sent on cross-site form submissions
// - Not sent when following external links
// Additional protection for sensitive operations:
app.post('/api/sensitive', requireAuth(), async (c) => {
// Verify origin header for extra security
const origin = c.req.header('origin')
const allowedOrigins = ['https://your-domain.com']
if (!allowedOrigins.includes(origin)) {
return c.json({ error: 'Invalid origin' }, 403)
}
// Process request...
})
Secret Management
Environment Variables
Never commit secrets to version control:
wrangler.toml
# wrangler.toml
name = "my-sonicjs-app"
[vars]
# Non-sensitive configuration
ENVIRONMENT = "production"
DEFAULT_FROM_EMAIL = "[email protected]"
# Sensitive values - use secrets instead!
# JWT_SECRET = "..." # NEVER DO THIS
Cloudflare Secrets
Use Wrangler to manage secrets:
Setting Secrets
# Set JWT secret
npx wrangler secret put JWT_SECRET
# Enter your secret when prompted
# Set SendGrid API key
npx wrangler secret put SENDGRID_API_KEY
# Set admin password
npx wrangler secret put ADMIN_PASSWORD
# For specific environment
npx wrangler secret put JWT_SECRET --env production
Required Secrets
| Secret | Purpose |
|---|---|
JWT_SECRET | Token signing key |
SENDGRID_API_KEY | Email delivery |
ADMIN_PASSWORD | Initial admin user |
Secret Rotation
JWT Secret Rotation
// When rotating JWT secret:
// 1. Add new secret
// 2. Update verification to accept both
// 3. Wait for old tokens to expire (24h)
// 4. Remove old secret
// Example: Accept multiple secrets during rotation
const secrets = [env.JWT_SECRET, env.JWT_SECRET_OLD].filter(Boolean)
for (const secret of secrets) {
const payload = await verify(token, secret)
if (payload) return payload
}
Rate Limiting
OTP Rate Limiting
The OTP plugin includes built-in rate limiting:
OTP Rate Limits
// OTP rate limiting configuration
{
rateLimitPerHour: 5, // Max OTP requests per hour
maxAttempts: 3, // Max verification attempts
expirationMinutes: 10 // OTP code lifetime
}
// Rate limit check
const recentRequests = await db
.prepare(`
SELECT COUNT(*) as count FROM otp_codes
WHERE email = ? AND created_at > datetime('now', '-1 hour')
`)
.bind(email)
.first()
if (recentRequests.count >= settings.rateLimitPerHour) {
return c.json({ error: 'Too many requests' }, 429)
}
Custom Rate Limiting
Implement rate limiting for your endpoints:
Rate Limit Middleware
import { RateLimiter } from '@sonicjs-cms/core'
// Create rate limiter with KV storage
const limiter = new RateLimiter(env.CACHE_KV, {
windowMs: 60000, // 1 minute window
maxRequests: 100 // Max 100 requests per window
})
// Apply to routes
app.use('/api/*', async (c, next) => {
const ip = c.req.header('cf-connecting-ip') || 'unknown'
const key = `rate:${ip}`
const allowed = await limiter.check(key)
if (!allowed) {
return c.json({ error: 'Rate limit exceeded' }, 429)
}
await next()
})
Brute Force Protection
Login Attempt Limiting
// Track failed login attempts
const MAX_ATTEMPTS = 5
const LOCKOUT_MINUTES = 15
app.post('/auth/login', async (c) => {
const { email, password } = await c.req.json()
// Check if locked out
const attempts = await getFailedAttempts(email)
if (attempts >= MAX_ATTEMPTS) {
const lockoutEnd = await getLockoutTime(email)
if (lockoutEnd > Date.now()) {
return c.json({
error: 'Account temporarily locked',
retryAfter: Math.ceil((lockoutEnd - Date.now()) / 1000)
}, 429)
}
}
const user = await verifyCredentials(email, password)
if (!user) {
await recordFailedAttempt(email)
return c.json({ error: 'Invalid credentials' }, 401)
}
// Clear failed attempts on success
await clearFailedAttempts(email)
return c.json({ token: generateToken(user) })
})
Production Checklist
Before deploying to production, verify:
Authentication & Secrets
- JWT_SECRET is set as a Wrangler secret (not default)
- Strong admin password is configured
- API keys (SendGrid, etc.) are stored as secrets
- HTTPS is enforced for all traffic
Configuration
- ENVIRONMENT is set to
production - Debug mode is disabled
- Error details are not exposed in responses
- CORS is configured correctly for your domains
Database
- All queries use parameterized statements
- Backups are configured and tested
- Migrations run successfully before deployment
Monitoring
- Logging is enabled for security events
- Failed login attempts are tracked
- Error monitoring is configured
- Rate limiting is enabled
Testing
- Penetration testing completed
- Input validation tested with edge cases
- Authentication flows tested thoroughly
- Authorization tested for each role
Security Verification Script
// Run before deployment
async function verifySecurityConfig(env: Bindings) {
const issues: string[] = []
// Check JWT secret
if (!env.JWT_SECRET || env.JWT_SECRET.includes('change-in-production')) {
issues.push('JWT_SECRET not properly configured')
}
// Check environment
if (env.ENVIRONMENT !== 'production') {
issues.push('ENVIRONMENT should be "production"')
}
// Check for debug mode
if (env.DEBUG === 'true') {
issues.push('DEBUG mode should be disabled')
}
if (issues.length > 0) {
console.error('Security issues found:', issues)
return false
}
console.log('Security configuration verified')
return true
}
Security Headers
Configure security headers for additional protection:
Security Headers
// Add security headers middleware
app.use('*', async (c, next) => {
await next()
// Security headers
c.header('X-Content-Type-Options', 'nosniff')
c.header('X-Frame-Options', 'DENY')
c.header('X-XSS-Protection', '1; mode=block')
c.header('Referrer-Policy', 'strict-origin-when-cross-origin')
c.header('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
// Content Security Policy
c.header('Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self' https:; " +
"connect-src 'self'"
)
})
Reporting Security Issues
If you discover a security vulnerability in SonicJS:
- Do not open a public issue
- Email security concerns to the maintainers
- Include detailed reproduction steps
- Allow time for a fix before disclosure