# Staff browser protections

Phase 6 milestone 24 adds response headers to the staff web application and aligns the prepared Caddy configuration. Milestone 25 adds a per-response script nonce and resource policy. No deployment or public PHP website change is included.

The Next server sends `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer` and a permissions policy disabling browser camera, microphone, geolocation, payment and USB access. The current workspace does not use these browser capabilities; native application permissions are separate. File uploads and attachment downloads remain supported.

The root workspace document sends this production policy, with a fresh random 32-byte base64 nonce in place of `RANDOM`:

```text
default-src 'self'; script-src 'self' 'nonce-RANDOM' 'strict-dynamic'; script-src-attr 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'
```

This refuses embedding, plugin content, injected base URLs and off-origin HTML form submission. Scripts need the current nonce; trusted scripts may load their dependencies through `strict-dynamic`. In modern CSP browsers, the `'self'` fallback does not authorize parser-injected scripts. Inline event handlers and string compilation (`eval`/`Function`) are blocked in production. Same-origin API connections remain available, as do local/blob/data images and file downloads.

`src/proxy.ts` generates the nonce using cryptographic randomness and overwrites the incoming policy before rendering. Caller-supplied nonce headers are removed. Prefetch labels do not bypass enforcement. The server layout waits for an incoming connection, so HTML is rendered per request instead of prerendered with a reusable nonce. Production documents are private/no-store; do not configure a CDN/proxy to cache or rewrite them. Static assets retain their independent cache behavior. Dynamic document rendering adds server work and needs hosted capacity validation.

The policy is deliberately scoped to the existing root workspace document. API attachment/PDF responses keep their stricter sandbox policies and no-store behavior; they must not inherit a replacement workspace policy. Shared headers apply to assets and API errors as well. Caddy uses the same referrer and permissions settings so the prepared proxy does not weaken them.

**Inline CSS remains allowed** for Radix inline styling and runtime scroll-lock styles. This is a script execution policy, not a complete inline-style lockdown or proof against XSS. Trusted script vulnerabilities and dependencies still need review; the policy does not replace safe rendering, authorization, origin checks or protected cookies. New document routes must receive an appropriate CSP when added. HTTPS and real proxy behavior still require hosted validation; no HSTS/subdomain changes are made to the public domain.

Development explicitly allows script `unsafe-eval` and loopback HMR WebSockets for the Next development runtime. These allowances depend only on `NODE_ENV=development`, not request headers. Development uses Next's no-cache/revalidation behavior. Never expose the development server as a deployed staff service.

## Verification

`tests/browser-protections.spec.ts` exercises Chromium refusal of an embedded workspace, injected base/plugin/form content, referrer omission, denied browser capabilities and normal authenticated sign-in. Prohibited destinations are intercepted synthetic routes; no third-party request is needed.

`tests/script-policy.spec.ts` runs with the production Playwright configuration. It checks fresh matching nonces on every script, hostile request/prefetch headers, no-store responses, stale nonce refusal, blocked inline/event-handler and same-origin/external parser-injected scripts, continued hydration/sign-in and blocked string compilation. Attack/probe scripts run through ordinary HTML parsing, not the DevTools evaluation mechanism that can bypass CSP.

The production configuration now runs the complete browser suite through a disposable local HTTPS proxy and standalone server, using an isolated synthetic test API. See [LOCAL-HTTPS.md](LOCAL-HTTPS.md) for the baseline, delivery and dispatch commands. Production Secure cookies work in both the browser and API fixture helpers over HTTPS. Certificate-error acceptance is limited to the explicitly configured loopback test contexts; no system trust or application cookie setting is weakened. The runner removes its temporary copy and certificate on normal shutdown. This is local production-web coverage, not Linux images, public TLS trust, deployed Caddy behavior or hosted acceptance.

`scripts/check-staging.py` validates prepared Compose/Caddy configuration and a production standalone server against a synthetic local API. It checks nonce/header/HTML agreement and freshness, no production eval, the headers on static assets and rejected authentication, correct JavaScript/CSS MIME types, production cookie flags, origin enforcement and authenticated attachment/PDF delivery retaining its sandbox, disposition and no-store policy. It never starts containers or issues certificates.

See [VALIDATION.md](VALIDATION.md) for dated results and [DEPENDENCY-REVIEW.md](DEPENDENCY-REVIEW.md) for unresolved mobile tooling findings. These browser changes do not repair either upstream dependency advisory.

References: [Next headers configuration](https://nextjs.org/docs/app/api-reference/config/next-config-js/headers), [CSP frame-ancestors](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-ancestors). Implementation follows the installed Next 16.3.8 documentation.
