|
| 1 | +# Security Response Headers |
| 2 | + |
| 3 | +Storefront Next ships default security response headers from the SDK. Every storefront generated from this template inherits them automatically — no opt-in needed. |
| 4 | + |
| 5 | +## What ships by default |
| 6 | + |
| 7 | +| Header | Default value | |
| 8 | +|---|---| |
| 9 | +| `Content-Security-Policy` | See [CSP directives](#csp-directives) below | |
| 10 | +| `Strict-Transport-Security` | `max-age=15552000; includeSubDomains` (only on Managed Runtime; suppressed locally) | |
| 11 | +| `X-Frame-Options` | `SAMEORIGIN` | |
| 12 | +| `X-Content-Type-Options` | `nosniff` | |
| 13 | +| `Referrer-Policy` | `strict-origin-when-cross-origin` | |
| 14 | +| `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` | |
| 15 | + |
| 16 | +### CSP directives |
| 17 | + |
| 18 | +| Directive | Default value | Why | |
| 19 | +|---|---|---| |
| 20 | +| `default-src` | `'self'` | Restricts every fetch type not otherwise listed. | |
| 21 | +| `script-src` | `'self' https://challenges.cloudflare.com 'nonce-<per-request>'` | Strict — no `'unsafe-inline'` or `'unsafe-eval'`. The per-request nonce permits the `__APP_CONFIG__` inline script. Cloudflare origin permits the Turnstile widget. | |
| 22 | +| `style-src` | `'self' 'unsafe-inline'` | Tailwind v4 + shadcn rely on inline styles. | |
| 23 | +| `img-src` | `'self' data: https://*.commercecloud.salesforce.com https://*.demandware.net` | DIS image URLs. | |
| 24 | +| `font-src` | `'self' data:` | Self-hosted web fonts. | |
| 25 | +| `connect-src` | `'self' https://*.commercecloud.salesforce.com https://*.demandware.net https://challenges.cloudflare.com` | SCAPI calls + browser-initiated XHR from the Turnstile widget. | |
| 26 | +| `frame-src` | `https://challenges.cloudflare.com` | Turnstile widget iframe. | |
| 27 | +| `frame-ancestors` | `'self'` | Modern equivalent of `X-Frame-Options`. | |
| 28 | +| `form-action` | `'self'` | Restricts form POST targets. CSP3 does NOT fall back to `default-src` for this directive — without it, forms can POST anywhere. | |
| 29 | +| `base-uri` | `'self'` | Prevents `<base href>` injection. | |
| 30 | +| `object-src` | `'none'` | Blocks Flash and other plugin content. | |
| 31 | +| `upgrade-insecure-requests` | enabled | Browser auto-upgrades HTTP subresources to HTTPS. | |
| 32 | + |
| 33 | +## Where the config lives |
| 34 | + |
| 35 | +All security-headers config is under `app.security.headers.*` in `config.server.ts`. (The sibling `app.security.turnstile.*` is unrelated — that's bot protection. Both live under `security` because they're defense-in-depth concerns.) |
| 36 | + |
| 37 | +## Extending CSP for a new origin |
| 38 | + |
| 39 | +Spread `defaultCspDirectives` and override per directive. **Each directive you set fully replaces the SDK default** — copy from the defaults to extend. |
| 40 | + |
| 41 | +```ts |
| 42 | +// config.server.ts |
| 43 | +import { defaultCspDirectives } from '@salesforce/storefront-next-runtime/security'; |
| 44 | + |
| 45 | +export default defineConfig<Config>({ |
| 46 | + app: { |
| 47 | + // … |
| 48 | + security: { |
| 49 | + turnstile: { /* …existing turnstile config… */ }, |
| 50 | + headers: { |
| 51 | + csp: { |
| 52 | + directives: { |
| 53 | + ...defaultCspDirectives, |
| 54 | + 'script-src': [ |
| 55 | + ...defaultCspDirectives['script-src']!, |
| 56 | + 'https://cdn.example.com', |
| 57 | + ], |
| 58 | + 'connect-src': [ |
| 59 | + ...defaultCspDirectives['connect-src']!, |
| 60 | + 'https://api.segment.io', |
| 61 | + ], |
| 62 | + }, |
| 63 | + }, |
| 64 | + }, |
| 65 | + }, |
| 66 | + }, |
| 67 | +}); |
| 68 | +``` |
| 69 | + |
| 70 | +## Disabling a header |
| 71 | + |
| 72 | +Set the field to `false` to disable a single header. Other headers remain. |
| 73 | + |
| 74 | +```ts |
| 75 | +security: { |
| 76 | + headers: { |
| 77 | + hsts: false, // disable HSTS only |
| 78 | + permissionsPolicy: false, // disable Permissions-Policy only |
| 79 | + }, |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +To disable everything (for debugging only): |
| 84 | + |
| 85 | +```ts |
| 86 | +security: { headers: { enabled: false } } |
| 87 | +``` |
| 88 | + |
| 89 | +A startup warning is logged whenever any header is disabled. |
| 90 | + |
| 91 | +## Migrating from PWA Kit |
| 92 | + |
| 93 | +PWA Kit shipped its own permissive defaults; Storefront Next defaults are stricter. For a safe rollout: |
| 94 | + |
| 95 | +1. Deploy with CSP in **report-only** mode: |
| 96 | + ```ts |
| 97 | + security: { headers: { csp: { reportOnly: true } } } |
| 98 | + ``` |
| 99 | + The browser won't block anything but will log all violations to DevTools (and to any configured `report-uri`). A startup warning is logged on every server boot in this mode. |
| 100 | +2. Watch DevTools / browser violation reports for a week or two. Identify legitimate origins your storefront uses (analytics CDNs, third-party widgets, etc.). |
| 101 | +3. Add those origins to your CSP via the spread pattern above. |
| 102 | +4. Flip `reportOnly: false` (or remove the field) and redeploy. Enforcement is now on. |
| 103 | + |
| 104 | +## Troubleshooting CSP violations |
| 105 | + |
| 106 | +Open the browser DevTools console. Violations look like: |
| 107 | + |
| 108 | +> Refused to load the script `https://cdn.example.com/foo.js` because it violates the following Content Security Policy directive: "script-src 'self' 'nonce-…' https://challenges.cloudflare.com". |
| 109 | +
|
| 110 | +The directive name in the message tells you what to extend. Common culprits: |
| 111 | + |
| 112 | +| Symptom | Likely fix | |
| 113 | +|---|---| |
| 114 | +| "Refused to load the script" | Add the origin to `script-src` | |
| 115 | +| "Refused to connect to" | Add the origin to `connect-src` | |
| 116 | +| "Refused to load the image" | Add the origin to `img-src` | |
| 117 | +| "Refused to apply inline style" | Custom inline styles need `'unsafe-inline'` (already on by default) — check the directive in the violation message | |
| 118 | + |
| 119 | +## Environment variable overrides |
| 120 | + |
| 121 | +Standard `PUBLIC__` env-var override applies. The path uses double-underscore separators following the existing `security.turnstile.*` pattern: |
| 122 | + |
| 123 | +```bash |
| 124 | +# Toggle CSP report-only mode (recommended use case for env vars): |
| 125 | +PUBLIC__app__security__headers__csp__reportOnly=true |
| 126 | + |
| 127 | +# Disable HSTS entirely: |
| 128 | +PUBLIC__app__security__headers__hsts=false |
| 129 | +``` |
| 130 | + |
| 131 | +Replacing individual CSP directive names is **not supported via env vars** because the env-var path uses `__` as a segment separator and CSP directive names contain hyphens (`script-src`, `connect-src`, …). Override the whole `directives` map as a JSON blob instead: |
| 132 | + |
| 133 | +```bash |
| 134 | +# Override the entire CSP directives map (note: this REPLACES all defaults): |
| 135 | +PUBLIC__app__security__headers__csp__directives='{"default-src":["'"'"'self'"'"'"],"script-src":["'"'"'self'"'"'","https://cdn.example.com"]}' |
| 136 | +``` |
| 137 | + |
| 138 | +Because this fully replaces the SDK defaults, env-var CSP overrides are best reserved for unusual cases. For most directive changes, edit `config.server.ts` and spread `defaultCspDirectives` — the shell escaping is unforgiving and the default-replacement semantics are easy to get wrong. |
| 139 | + |
| 140 | +See [README-CONFIG.md](./README-CONFIG.md) for the full env-var override system. |
| 141 | + |
| 142 | +## Related docs |
| 143 | + |
| 144 | +- [README-CONFIG.md](./README-CONFIG.md) — Configuration system |
| 145 | +- [README-CONFIG-OPTIONS.md](./README-CONFIG-OPTIONS.md) — Configuration reference |
| 146 | +- [README-AUTH.md](./README-AUTH.md) — Auth and social-login (extends `connect-src`) |
| 147 | +- [README-TURNSTILE.md](./README-TURNSTILE.md) — Cloudflare Turnstile (already permitted in defaults) |
0 commit comments