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.
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 ishandleRequest(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 thecontextit receives:solid-vite-plugin/src/ssr/index.ts
Line 932 in e4cdee4
solid-vite-plugin/src/ssr/index.ts
Line 1394 in e4cdee4
So even when a host passes
nonce,renderToStreamnever gets it. The hydration bootstrap (_$HY), the streamed data and swap scripts and themodulepreloadlinks 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:
mod.fetch(req)) never pass options. That is intentional, since the second argument of a Fetchable belongs to the host:solid-vite-plugin/src/ssr/index.ts
Line 1461 in e4cdee4
start.middlewareis the natural place to generate the nonce and set theContent-Security-Policyheader, but it only sees(request, next). The options bag is fixed before the chain runs.start.setupreturns a component andstart.renderModereturns a mode, so neither can change the stream options.@solidjs/webonly reads the nonce from therenderToStreamoptions, not from the request event orlocals.Today the workaround is hand-written
entry-server/entry-clientfiles that passnoncetorenderToStream. Entries come in pairs, so both have to be written, and authored entries also give up the generatedDefaultErrorBoundary. 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
CSPNoncethat@solidjs/webaccepts ({ script, style }) throws in the client-entry transform, because it escapes the value as a string:solid-vite-plugin/src/ssr/index.ts
Line 1279 in e4cdee4
Reproduction
With
@solidjs/vite-plugin3.0.0-next.47 and@solidjs/web2.0.0-rc.13, inexamples/start-ssr:Proposal
A
start.nonceoption that follows therenderModemodule convention. The option names a module that default-exports(event) => CSPNonce | undefined | Promise<…>. The handler resolves it after the middleware chain, next toresolveRenderMode, so a middleware can generate the nonce, set the header and store it onevent.locals:The resolved value (or
handleRequest'snonce, which keeps precedence) would go to:renderToStream;createSSRResponse, both throughscriptNonce, which also fixes the object form;csp-noncemeta for the styles the Vite client injects);context.nonce.I have a PR ready and will link it here.