Skip to content

Simple guide · Overview

Page status: Active

Security Hardening

Issue #106 adds a focused security pass for the NutsNews web app.

Visual overview

Primary diagram

System map

Issue #106 adds a focused security pass for the NutsNews web app.

Render the repository-owned system map when you need it.

Diagram is not rendered yet.

View as text
Security Hardening

Issue #106 adds a focused security pass for the NutsNews web app.

flowchart TB
  accTitle: Security Hardening
  accDescr {
    Issue #106 adds a focused security pass for the NutsNews web app.
  }
  A["Security Hardening"] --> B["SECURITY_HARDENING.md"]
  B --> C["Auto-generated placeholder diagram"]

Security Hardening

Fullscreen diagram view.

Issue #106 adds a focused security pass for the NutsNews web app.

This update adds:

  • centralized browser security headers
  • Content Security Policy
  • no-store behavior for admin and operational routes
  • noindex behavior for admin routes
  • stronger admin route auth boundaries
  • contact form origin, size, rate-limit, timeout, honeypot, and Turnstile controls
  • a repeatable security header script in Web CI

Security headers live in web/lib/securityHeaders.ts.

They are applied from web/middleware.ts.

The baseline includes:

HeaderPurpose
Content-Security-PolicyRestricts scripts, styles, images, frames, connections, and form submissions.
X-Frame-OptionsBlocks legacy framing and clickjacking attempts.
X-Content-Type-OptionsPrevents MIME sniffing.
Referrer-PolicyLimits referrer leakage.
Permissions-PolicyDisables unused browser features such as camera, microphone, geolocation, payment, USB, and display capture.
Strict-Transport-SecurityRequires HTTPS in production.
Cross-Origin-Opener-PolicyUses same-origin-allow-popups so OAuth popups can keep working.
Cross-Origin-Resource-PolicyProtects same-origin resources.

The CSP is intentionally narrow while allowing NutsNews dependencies:

DirectiveAllowed use
script-srcApp scripts, Google Analytics/Tag Manager, and Cloudflare Turnstile.
style-srcApp styles and inline styles required by the current UI.
img-srcApp images, data/blob images, and HTTPS publisher thumbnails.
connect-srcApp APIs, Supabase, Sentry, Google Analytics, and Turnstile.
frame-srcCloudflare Turnstile challenge frames only.
frame-ancestorsnone, so NutsNews cannot be embedded by another site.
object-srcnone.
base-uriself.
form-actionself.

Publisher thumbnails come from many original article sources, so img-src allows HTTPS images broadly. Script, frame, and connection sources stay limited to known services.

Protected admin pages live under web/app/admin/(protected).

The protected layout checks:

  1. the user has a session
  2. the session has an email
  3. the email is allowed by ADMIN_EMAILS

Admin pages are forced dynamic and disable revalidation. Middleware also sends no-store cache headers for /admin routes and adds X-Robots-Tag: noindex, nofollow, noarchive.

The contact API lives at web/app/api/contact/route.ts.

Controls include:

  • JSON-only requests
  • actual request body size limit
  • email validation
  • message length limits
  • honeypot field
  • same-origin checks with optional NUTSNEWS_ALLOWED_CONTACT_ORIGINS
  • per-IP/per-email in-memory rate limiting
  • Turnstile token length limit
  • Turnstile server-side Siteverify call
  • request timeout for Turnstile
  • request timeout for Resend
  • no-store response headers

Turnstile secrets stay server-side. The browser sends a token, and the server validates it before sending email.

Run locally:

cd web && npm run test:security-headers

The same check runs in .github/workflows/web-ci.yml before build.