From 0115920dd34dedb4f7c5871cbc257136e1031f74 Mon Sep 17 00:00:00 2001 From: Daniil Gaponov Date: Sun, 6 Sep 2026 13:29:47 +0300 Subject: [PATCH] feat(security): add security response headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The site sent no security headers at all: no HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy or Permissions-Policy, and it disclosed its framework via X-Powered-By. Adds a headers() rule for all routes plus poweredByHeader: false. Two deliberate deviations from the usual boilerplate: - X-Frame-Options is SAMEORIGIN, not DENY. SandboxBlock embeds `${window.location.origin}/sandbox//` in an iframe on every component page, and DENY blocks same-origin framing too, so DENY would break every live component demo. - HSTS omits `preload`. Submitting to the preload list is effectively irreversible, and max-age is 1 year rather than 2. `includeSubDomains` is set, which assumes every subdomain is HTTPS-only — verify before deploy. CSP is not included; it needs its own pass to build an allow-list for storage.yandexcloud.net and GTM. Verified in dev and in a production build: all five headers present on pages and on the sandbox route, X-Powered-By gone everywhere, and the sandbox iframe still loads and hydrates (checked in a real browser: contentDocument accessible, no framing errors in console). Build, lint and typecheck pass. E2E not run — Playwright browsers are not installed locally. Co-Authored-By: Claude Opus 5 (1M context) --- next.config.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/next.config.js b/next.config.js index 8373ceec57cb..c00a3cc45f3b 100644 --- a/next.config.js +++ b/next.config.js @@ -105,6 +105,37 @@ module.exports = withBundleAnalyzer({ return config; }, + async headers() { + return [ + { + source: '/:path*', + headers: [ + { + key: 'Strict-Transport-Security', + // `preload` is deliberately omitted: submitting to the preload list is + // effectively irreversible. Add it only after confirming every + // subdomain is HTTPS-only and you intend to submit. + value: 'max-age=31536000; includeSubDomains', + }, + {key: 'X-Content-Type-Options', value: 'nosniff'}, + { + key: 'X-Frame-Options', + // Must stay SAMEORIGIN, not DENY: SandboxBlock embeds + // `${window.location.origin}/sandbox//` in an iframe + // on every component page, and DENY blocks same-origin framing too. + value: 'SAMEORIGIN', + }, + {key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin'}, + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=()', + }, + ], + }, + ]; + }, + // Drops the `X-Powered-By: Next.js` version-disclosure header. + poweredByHeader: false, reactStrictMode: true, // The theme builder route was renamed /themer -> /themes; keep old // links and indexed URLs working with a permanent redirect.