The website uses defense in depth. No website can be guaranteed safe against every future exploit. This document describes the active protection layers and the remaining operational tasks.
The editor is intentionally not a remote CMS. It starts only with:
npm install
npm run security:setup-editor
npm run editorAll Node dependencies live locally under node_modules/. The setup command writes a scrypt password hash and random session secret to .env. The plaintext password is not stored. .env is excluded by .gitignore.
Editor address:
http://127.0.0.1:8081/editor/
The editor binds only to 127.0.0.1. Router middleware also rejects non-local access. Editor pages and editor APIs require login. Write requests additionally require a valid CSRF token and matching origin.
EDITOR_LOCAL_ONLY=true is required whenever EDITOR_MODE=1. Editor requests with Forwarded or X-Forwarded-* headers are rejected so a public reverse proxy cannot accidentally turn proxy-local traffic into editor access. Do not proxy /editor or /api/editor from the public website.
Remote editor access should use a private channel, not a public route. Example SSH tunnel:
ssh -L 8081:127.0.0.1:8081 user@serverThen open:
http://127.0.0.1:8081/editor/
In Docker, use the explicit Compose editor profile. It publishes the editor on host 127.0.0.1 only and sets EDITOR_ALLOW_DOCKER_BRIDGE=1 so Docker's private bridge gateway can pass the localhost guard. Do not publish that profile on 0.0.0.0 or through a public reverse proxy.
Rotate the password or session secret:
npm run security:setup-editorThen restart the running editor process. Existing sessions become invalid.
Rotate external secrets at their provider too if they were exposed: SMTP password, CAPTCHA provider secret, hosting tokens, or DNS/API tokens. After rotation, update .env, restart the service, and verify contact forms still work.
- Node responses set CSP, MIME protection, frame rules, referrer policy, permissions policy, and HSTS in production.
public/_headerscontains equivalent headers for Netlify-compatible static hosts.- Rich HTML fields are sanitized server-side on save and client-side on render.
- Links, assets, CSS image positions, and iframe sources are processed through shared allowlist helpers.
- Uploads allow only JPG, PNG, WebP, and GIF, up to 8 MB, with matching file signatures. SVG uploads are blocked.
- Contact forms have size limits, field limits, server-side validation, and an IP-based rate limit.
- Production contact submissions require a same-origin
OriginorRefererheader. - Editor login has per-IP throttling, global throttling, progressive lockouts, generic errors and failed-login security logs.
- Editor API and media upload endpoints have additional authenticated rate limits.
- Instagram requests have timeouts. Image proxying accepts only expected Instagram CDN hosts and limits download sizes.
- Security logs are structured JSON on stderr/stdout and intentionally omit passwords, cookies, tokens and captcha values.
Run the public Node website with npm run start behind HTTPS. A reverse proxy must terminate TLS and preserve the standard headers. SMTP, hCaptcha, and mCaptcha secrets belong only in the server environment.
Important environment variables:
NODE_ENV=production
TRUST_PROXY=false
SMTP_HOST=smtp.example.org
SMTP_PORT=587
SMTP_SECURE=false
SMTP_REQUIRE_TLS=true
SMTP_USER=mail-user
SMTP_PASS=mail-password
SMTP_FROM=website@example.org
CONTACT_TO=contact@example.org
CAPTCHA_PROVIDER=hcaptcha
HCAPTCHA_SITE_KEY=public-site-key
HCAPTCHA_SECRET=private-secret
CONTACT_ALLOW_WITHOUT_CAPTCHA=0Use TRUST_PROXY=loopback when a local reverse proxy such as Caddy or Nginx forwards to the Node process on the same host. Use TRUST_PROXY=private only when requests come through a trusted private proxy network. Do not use broad true proxy trust.
In production, contact mail configured through SMTP_HOST and CONTACT_TO requires a selected CAPTCHA provider unless CONTACT_ALLOW_WITHOUT_CAPTCHA=1 is deliberately set. That opt-out is intended only for deployments with equivalent upstream abuse protection.
CAPTCHA provider selection:
CAPTCHA_PROVIDER=none: no server-side CAPTCHA verification. Use this with production mail only when upstream rate limits, WAF, or another abuse control exists.CAPTCHA_PROVIDER=hcaptcha: requiresHCAPTCHA_SITE_KEYandHCAPTCHA_SECRET.CAPTCHA_PROVIDER=mcaptcha: requiresMCAPTCHA_VERIFY_URL,MCAPTCHA_SITE_KEY,MCAPTCHA_SECRET, andMCAPTCHA_WIDGET_URL.MCAPTCHA_SCRIPT_URLis optional.- If
CAPTCHA_PROVIDERis unset, the server auto-selects none, hCaptcha, or mCaptcha only when the config is unambiguous.
For Node/Docker deployments, CAPTCHA env vars are the source of truth. The server injects public CAPTCHA config into served JSON responses; do not duplicate CAPTCHA config in local JSON. Static-only hosting can use the browser-visible captcha object in ignored local JSON because there is no server env at runtime.
The editor does not belong on a public server. Remote administration needs a separate design with HTTPS, persistent storage, proxy configuration, and additional access control.
The editor writes backups under .editor-backups/ before file changes. These files and .env must not be publicly served. Operating-system file permissions, server updates, and backup retention remain operator responsibilities.
Local JSON overrides, generated event pages, and deployment-specific media are also excluded by .gitignore. Before a public push, check:
npm run release:check-public
git status --short --ignoredThe guard blocks known local paths from the Git publication scope. It does not replace manual review of newly added text, URLs, or media.
Before deployments:
npm run check
npm test
npm run test:e2e
npm audit --omit=dev
npm run release:check-publicUpdate dependencies regularly and review security advisories. If a secret leaks, rotate .env, restart the process, and replace affected external credentials too.
- Local filesystem or user-account access is outside the website boundary.
- Third-party embeds load code or content from their providers.
- Static hosts must actually apply
public/_headersor configure equivalent headers. - CAPTCHA protection is active only when the selected provider has its server-side secret configured and the matching public widget config is delivered to the browser.
- Static hosts using mCaptcha must add the mCaptcha script origin to
script-src, the widget origin toframe-src, and the verify/widget origins toconnect-srcin their_headersor equivalent host configuration.
After DNS and TLS are live, verify the public response:
curl -I https://simonswelt.de/Confirm these headers are present:
Content-Security-PolicyStrict-Transport-SecurityX-Content-Type-Options: nosniffX-Frame-OptionsReferrer-PolicyPermissions-Policy
Also confirm HTTP redirects to HTTPS at the reverse proxy or hosting provider.
- Rotate editor password/session secret with
npm run security:setup-editor. - Rotate any SMTP, hCaptcha, mCaptcha, or editor secret that may have been shared outside the host.
- Keep
.env,.editor-backups/, local JSON overrides, generated pages and private media out of Git and public static hosting. - Set
NODE_ENV=production,TRUST_PROXY, SMTP variables, and one CAPTCHA mode (none,hcaptcha, ormcaptcha). - Keep
EDITOR_MODEoff for the public website. If the editor is started separately, setEDITOR_LOCAL_ONLY=true. - Do not proxy
/editoror/api/editorfrom the public domain. - Run
npm run release:check, then inspectgit status --short --ignored. - Keep CDN/WAF or host-level request limits enabled for public traffic.