MPLS CMS was built for a journalist covering politically sensitive topics. Every security feature exists because the threat model demanded it — not as an afterthought or optional plugin.
- Redis-backed sessions — 32-byte crypto/rand tokens stored in Redis with 24-hour sliding TTL. No JWTs, no client-side token storage.
- bcrypt cost 12 — password hashing with a cost factor that takes ~250ms per hash, making brute force impractical.
- TOTP 2FA — optional per-user two-factor authentication using standard TOTP (Google Authenticator, Authy, etc.). Secrets encrypted at rest with AES-256-GCM.
- HttpOnly + Secure + SameSite=Lax cookies — session cookies are inaccessible to JavaScript and only sent over HTTPS.
- Session management — users can view active sessions, revoke individual sessions, or revoke all sessions.
- Role-based access control — three roles: admin, editor, contributor. Contributors can only edit their own posts. Editors can publish. Admins manage users and settings.
- Per-route middleware —
RequireRole("admin")applied at the route group level, not checked inline in handlers.
- Double-submit cookie — CSRF token set as a cookie on GET requests and validated against a form field or
X-CSRF-Tokenheader on POST/PUT/DELETE. - SameSite=Strict on cookie — prevents cross-origin cookie sending.
- Token stability — the CSRF token is reused for the cookie's lifetime (1 hour), not rotated on every GET. This prevents token invalidation when fetch-based requests (like media picker loading) trigger a GET that would otherwise rotate the cookie out from under the form.
- Redis sliding window — configurable per-route rate limits. Default limits:
- Login: 5 attempts per 15 minutes per IP
- Contact form: 3 submissions per hour per IP
- Newsletter subscribe: 5 per hour per IP
- Search: 30 per minute per IP
- API: 60 per minute per IP
- Strict CSP built from config slices — no wildcard
*domains. - Separate config for
img-src,frame-src,script-src,style-src,font-src,connect-src. - CSP violations are reported but not blocked in development mode.
Every response includes:
X-Frame-Options: DENYX-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-originX-XSS-Protection: 0(modern browsers use CSP instead)Strict-Transport-Security: max-age=31536000; includeSubDomains; preload(production only)
- bluemonday — all rich text editor output is sanitized server-side before storage.
- Custom policy allows YouTube/Twitch iframe embeds from whitelisted domains only.
- No client-side sanitization — the server is the single source of truth.
- MIME detection by content — file type is detected from the first 512 bytes, not the file extension.
- UUID filenames — uploaded files are renamed to
{year}/{month}/{uuid}.{ext}, preventing path traversal and filename collisions. - Configurable size limit —
MAX_UPLOAD_BYTESdefaults to 10MB. - R2/S3 or local disk — uploads go to Cloudflare R2 (S3-compatible) or local disk, configurable per environment.
- ALTCHA proof-of-work — client-side computational challenge on forms (contact, comments). Not a CAPTCHA — no image puzzles. The client solves a hash challenge that proves it spent CPU time.
- Honeypot fields — hidden form fields that bots fill in. Submissions with honeypot data are silently accepted (no error feedback to the bot).
- Content checks — basic spam pattern detection (all-caps, URL density, known spam phrases).
- Every admin action is logged with user ID, action, target, details JSON, IP address, and timestamp.
- Audit entries are published to the SSE hub for real-time monitoring.
- Hashed tokens — reset tokens are hashed before storage (bcrypt). The plaintext token is sent to the user's email; the hash is stored in the database.
- 1-hour expiry — tokens expire after one hour.
- Anti-enumeration — the same response is returned whether the email exists or not.
For pre-launch or staging environments, the site gate requires a password to access any page:
SITE_GATE_PASSWORD=preview123When set, all visitors see a password prompt before accessing the site. Admin routes and health checks bypass the gate.
The framework reads all secrets from environment variables. In production, use a secrets manager like Infisical, Vault, or AWS Secrets Manager to inject them.
Required secrets:
SESSION_SECRET— 32+ bytes, used to derive session storage keysCSRF_SECRET— 32+ bytes (currently used for the CSRF cookie domain)TOTP_ENCRYPTION_KEY— exactly 32 bytes (hex-encoded), used for AES-256-GCM encryption of TOTP secrets
Generate with:
openssl rand -hex 32- No WAF — use Cloudflare, AWS WAF, or nginx rate limiting for network-level protection.
- No DDoS protection — use a CDN or reverse proxy.
- No automatic dependency updates — use Dependabot or Renovate.
- No penetration testing — hire a professional.
- No encryption at rest — use PostgreSQL's built-in encryption or full-disk encryption at the OS level.