Skip to content

Start mode: generated entries render without the CSP nonce, and there is no per-request seam to supply one #388

Description

@everton-dgn

Problem

A strict CSP (script-src 'nonce-…' 'strict-dynamic', no 'unsafe-inline') cannot be served with Start mode's generated entries. The only nonce input is handleRequest(request, { nonce }), added in #311, and it only reaches two tags: the injected client-entry <script> and the post-flush redirect fallback.

The generated entry renders with a fixed { manifest } and ignores the context it receives:

const streamOptions = `{ manifest${serverComponents ? ', plugins: [ServerComponentPlugin]' : ''} }`;

` let result = entry.render(request, { clientEntry, ...options.context });`,

So even when a host passes nonce, renderToStream never gets it. The hydration bootstrap (_$HY), the streamed data and swap scripts and the modulepreload links all go out without a nonce, and the page does not hydrate under the policy.

There is also no way to supply the nonce from inside the app:

  • Deployments that dispatch through the default Fetchable (Nitro calls mod.fetch(req)) never pass options. That is intentional, since the second argument of a Fetchable belongs to the host:
    ` return handleRequest(request);`,
  • start.middleware is the natural place to generate the nonce and set the Content-Security-Policy header, but it only sees (request, next). The options bag is fixed before the chain runs.
  • start.setup returns a component and start.renderMode returns a mode, so neither can change the stream options.
  • @solidjs/web only reads the nonce from the renderToStream options, not from the request event or locals.

Today the workaround is hand-written entry-server / entry-client files that pass nonce to renderToStream. Entries come in pairs, so both have to be written, and authored entries also give up the generated DefaultErrorBoundary. SolidStart had a per-request seam for this (createHandler(fn, (event) => ({ nonce })), see solidjs/solid-start#2252). #311 ported the client-entry part of that change.

A related bug: the object form CSPNonce that @solidjs/web accepts ({ script, style }) throws in the client-entry transform, because it escapes the value as a string:

` const nonceAttr = nonce ? ' nonce="' + escapeAttribute(nonce) + '"' : '';`,

TypeError: value.replace is not a function

Reproduction

With @solidjs/vite-plugin 3.0.0-next.47 and @solidjs/web 2.0.0-rc.13, in examples/start-ssr:

const html = await (await handleRequest(new Request('http://localhost/'), { nonce: 'abc' })).text();
// only the client-entry <script> carries nonce="abc";
// the _$HY bootstrap, the data scripts and the modulepreload links do not

await handleRequest(new Request('http://localhost/'), { nonce: { script: 'abc', style: false } });
// TypeError: value.replace is not a function

Proposal

A start.nonce option that follows the renderMode module convention. The option names a module that default-exports (event) => CSPNonce | undefined | Promise<…>. The handler resolves it after the middleware chain, next to resolveRenderMode, so a middleware can generate the nonce, set the header and store it on event.locals:

solid({ start: { middleware: './src/middleware.ts', nonce: './src/nonce.ts' }, ssr: true });

// src/nonce.ts
export default (event: RequestEvent) => event.locals.nonce as string | undefined;

The resolved value (or handleRequest's nonce, which keeps precedence) would go to:

  • the generated entry's renderToStream;
  • the client-entry tag and createSSRResponse, both through scriptNonce, which also fixes the object form;
  • in dev, the head tags the handler injects (the style patch and Vite client scripts, the collected styles, and a csp-nonce meta for the styles the Vite client injects);
  • authored entries, as context.nonce.

I have a PR ready and will link it here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions