nibtime/next-safe-middleware

Static CSP manifest

Closed this issue · 1 comments

Motivation

as req.page.name got deprecated in 12.2 stable middleware, I was coerced to internally redesign the script and style hashes file writing + fetching and drop the old "per route" approach. While I did, I realized it was a bit flaky anyway, but it was good enough to get something running. Concurrent file writing was happening without locking of the critical file access section, which is bad, but is now solved - just went unnoticed most of the time, because the writing was distributed over files per route. Now all concurrent worker processes write into a single file what hashes they observe during build-time prerendering in a safe manner.

The idea

create a single automatic _next/static/csp/manifest.json that collects all relevant information for CSP during build-time prerendering. It will include the following:

  • hashes of all scripts that load before Next.js is interactive, including Next.js framework scripts/chunks themselves (currently _next/static/csp/script_hashes.txt) script-src
  • hashes of all style elements and inline style attributes (currently _next/static/csp/style_hashes.txt) for style-src
  • origins of all <link rel="stylesheet" src="https://"> for style-src
  • origins of all <img src="https://"> elements for img-src

some preprocessing is safer than others and you could decide per directive, whether to process any HTML, just from _document context or initialProps.html.

processing for script-src

You would never want to pickup any scripts except Next.js framework, those with next/script beforeInteractive and custom scripts in <Head> of _document. You never want to process initialProps.html here for security reasons

processing for style-src

To avoid unsafe-inline for style-src, you have to process initialProps.html, or else even the standard Next.js 404 page won't render with styles in CSP enforce mode. In this case, you would pair it with SSR sanitization (#41) to make sure, nobody can sneak in malicious inline styles if you SSR render user data from getStaticProps or getServerSideProps.

With the alternative unsafe-inline an attacker could do so anyway and even without SSR sanitization, the CSP would still block all malicious styles inserted during CSR (client-side rendering).

It can make sense here to distinguish between <style> tags and style attributes, i.e. never trustify <style> tags in initialProps.html, only in _document and next/head, but do it for style attributes (arbitrary CSS can do lot more harm). Then we could also additonally set the style-src-elem and style-src-attr directives, supported browsers will then override style-src.

We can look for in _document and next/head and safely add those as sources

processing for img-src

Whether you want this will highly depend on the type of app. If it's an app with static pages + CMS that only uses static images and optimized images from CMS, imgix, etc., or a static docs site with Nextra (like #30) it makes sense to automatically collect sources from initialProps.html.

If It is an app where users can upload images and those images get rendered during SSR, it would basically be trustyifing whatever users upload, which defies the purpose.

Maybe making this opt-in per route could make sense?

I wrote up a redesign into smaller packages that will make the lib a lot more versatile and robust towards Next release cycles and breakages: #60.

The static CSP manifest in .next/static/~csp/csp-manifest.json is the missing part from Next for CSP and encapsulates the missing the framework integration.

That especially enables post-build processing for generating header files to enable Hash-based Strict CSP with reporting for static sites with next export. I am working on loading Next by trusted inline proxy to support Firefox and Safari as well.

Here is a sample manifest from a build on the branch I am working on:

{
  "scripts": [
    { "hash": "sha256-2AH2VH1B+5jBCXgwJkVuj1TRGO0aUm9dcNatWrnyosU=" },
    { "hash": "sha256-9w/2kus6aGDwUPnOCCQherDxdWbCvLCBCmCs6sulDPk=" },
    {
      "src": "/_next/static/chunks/polyfills-0d1b80a048d4787e.js",
      "hash": "sha256-9KBZhMnIhr6i6Vtn/+m7sBLmWhnSfquC+BYUfYZs21E="
    },
    { "hash": "sha256-+n6qCniDL16DO3FYEyaFapnzcUr0xSMx30D68AgajH4=" },
    { "hash": "sha256-caZ4jK/H2c0EBwQO1MZvBmQ1lYAU1ZCArDJYdCuOasU=" },
    { "hash": "sha256-IFjfFXH44WhYeDwlSXGcMJYkO2TrragE7KfOMymKqeo=" },
    {
      "src": "https://browser.sentry-cdn.com/6.16.1/bundle.min.js",
      "hash": "sha256-JAaezFopPjKiakZP+b4Ci0ud+8thZIs1C5VysH+1y/0="
    },
    {
      "src": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js",
      "hash": "sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
    },
    { "hash": "sha256-g+gpY5BEvH6nxJcyYIVtyA82RzaHq99FUVNggdmRE+A=" },
    { "hash": "sha256-/ct9xY8uuUyzidlQETGHjFJ0wyDRQZEm/FKQX91cwFs=" },
    {
      "src": "/_next/static/chunks/webpack-fec509acdb9da2b9.js",
      "hash": "sha256-76bIP8g007l1bGKevmGsyH4yp2/loFKsYA+t+OMOWC0="
    },
    {
      "src": "/_next/static/chunks/framework-a5c6f778e64c64aa.js",
      "hash": "sha256-ltBZA3C4sBfvNcb6DapzvXFAsxaUFHOtERGQ3NdIW9o="
    },
    {
      "src": "/_next/static/chunks/main-ebd704491b14f85a.js",
      "hash": "sha256-lEfi5yeEXCdbhRbRiPrgUqEmDEqVNUF8EOxGKU4QOH0="
    },
    {
      "src": "/_next/static/chunks/pages/_app-0d69aa90602987ff.js",
      "hash": "sha256-XR7ftX8KuI0C3mk6vbZ9SNGYTiXQJIWvcLJPJj+/kYw="
    },
    {
      "src": "/_next/static/chunks/350-b6275529b99e3b05.js",
      "hash": "sha256-Qr/EpSuLHUc1sCz4PXbARxrZcHqVOWMrOtC4jWi9Hn0="
    },
    {
      "src": "/_next/static/chunks/601-c327ee4fe938e7f4.js",
      "hash": "sha256-y10VdR9eecrlndJwPgTcRvmwsA0HpyZ+E1Wi+kfvwY0="
    },
    {
      "src": "/_next/static/chunks/pages/isr/%5Bslug%5D-630c7358d6325501.js",
      "hash": "sha256-ZkMx66UN7JUP+KWmrPYQuJYgdZ66EvkajcKa84XOD54="
    },
    {
      "src": "/_next/static/chunks/pages/gsp-3e75edc8c9fa94bd.js",
      "hash": "sha256-MfcGdBBaiVQRP2iT3kiaVGYV1sXAYPy5kqinpetbpIw="
    },
    {
      "src": "/_next/static/chunks/291-787a106c2cd9ba8d.js",
      "hash": "sha256-YXZk2gENmwu4EF2La/JCdiplSwXWaMfOHMGfuZYnVDQ="
    },
    {
      "src": "/_next/static/chunks/pages/mantine/gsp-88dac9be2e0c53d0.js",
      "hash": "sha256-abUFcs/OnRPrQhJcbtprzZ2hj0N57upqT7E2q09Bjm4="
    },
    {
      "src": "/_next/static/chunks/pages/index-8f4bec49a2de0b59.js",
      "hash": "sha256-3gWY0W/64Qubx48wBZvr1bB667i+VpNFDb2LDanyvhU="
    },
    {
      "src": "/_next/static/chunks/pages/mantine-0344c2794e48d414.js",
      "hash": "sha256-XaLh7gM+8odY9RV6SercurJTnx8tbp/lhdO/uX2rq0g="
    },
    {
      "src": "/_next/static/chunks/pages/404-4d12610dcbec16f6.js",
      "hash": "sha256-ODHZG48TfgrpLeU5fh/1O+Mij5ml/bvg1DC9lREXVZk="
    }
  ],
  "styles": {
    "elem": [
      "sha256-NskH1lcdqXW4iCQ/xo7bzwdvYhkECzDWlXWcAbBsq/c=",
      "sha256-e4MNKvxl4YyYix43aa973iCw9bszi12hWIyvwN0bIOU=",
      "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
      "sha256-0h0Ha1KEXibxI1pkH1Ag1lqpMsbVx7SR13/64S1f0iI=",
      "sha256-tnHpksLcaJdz/J2Ur+0LthxAfZ7tQIxLHOipbQYvmqM=",
      "sha256-RkT3nEyK1ezLVIUunAjAbjB38Tn06a3wknKrzP1l3B8=",
      "sha256-SXejsnnv2/sVjwk10VmMC62z+5Q2IPMsojs8mHoNxxQ=",
      "sha256-CSf4Y5FwT5GZNsRsuDCG+IH7SEyo+hmB7ID/yVUACUk=",
      "sha256-Rtt+NP7X9ak8M6cTE1Pvn7d/A2yBJc2Xt1N1qWStGI8=",
      "sha256-KEG6QNIqOqlNXBOokXQIuSCHwQPt5NFuOcDJZYPhilU=",
      "sha256-mIxvY55smipKViZPiWrIlT0pyrPW7tFDSucq4IJ4Dvc=",
      "sha256-jPai78XjgtC5gmJ/wVphWGF83G4ouEjjS1vWqsa/DdY=",
      "sha256-CZYhQxojebgodVAtuYIfwG7S4U7MDLc/tyfpDMZc12M="
    ],
    "attr": [
      "sha256-OhYnFlXYqsFp+GhnHx7gjkLP36zGF+bnCBieql8Ye/4=",
      "sha256-/cz+p719dOFygDAqDgEjHhHSRaka+kWXk3WHAOXiURk=",
      "sha256-+sWhfTcZSG7XrsT61RI144ba9rE54ohM2kU43W6Do4U=",
      "sha256-Y87ijHEvOg7v5fuWx0A1v925QdejXEJEUkcoZSNiHtc=",
      "sha256-Di1xujw891gUw2f4Dcl3e05ECLSB4DK5RmDJ02qCl+M=",
      "sha256-4wz1PNEVsngKpNW7GiQDSpaCNpej+0TqJXz11x4oVKw="
    ]
  }
}