vike icon indicating copy to clipboard operation
vike copied to clipboard

CSP nonce support

Open JonWatkins opened this issue 11 months ago • 6 comments

Description

It would be good if the renderPage was able to apply CSP nonce to the relevent tags. We are using helmet with express in our application, and i have managed to get it to work by doing something linke this

const pageContext = await renderPage(pageContextInit);
const { httpResponse } = pageContext;

if (!httpResponse) {
    return next();
  } else {
    const { body, statusCode, headers, earlyHints } = httpResponse;

    if (res.writeEarlyHints) {
      const links: string[] = earlyHints.map((e) => e.earlyHintLink);
      res.writeEarlyHints({ link: links });
    }

    headers.forEach(([name, value]) => res.setHeader(name, value));

    res.status(statusCode);
    res.send(applyCSPToHTML(body, res.locals.cspNonce));
  }
import { parse, serialize } from "parse5";

export const applyCSPToHTML = (html: string, nonce: string): string => {
  const document = parse(html);

  // Function to apply nonce attribute to script tags
  const applyNonceToScripts = (node: any) => {
    if (node.tagName === "script") {
      const nonceAttribute = node.attrs.find(
        (attr: any) => attr.name === "nonce",
      );
      if (!nonceAttribute) {
        node.attrs.push({ name: "nonce", value: nonce });
      }
    }
    if (node.childNodes) {
      node.childNodes.forEach(applyNonceToScripts);
    }
  };

  // Apply nonce attribute to script tags
  applyNonceToScripts(document);

  // Serialize the modified document back to HTML
  const modifiedHTML = serialize(document);
  return modifiedHTML;
};

Im aware, that I would be able to include a meta tag in the HTML easy enough, but applying a nonce to tags generated by Vike is a little more awkward currently.

JonWatkins avatar Mar 15 '24 15:03 JonWatkins

I agree that'd be nice.

I think the following would be a simple solution.

  • Setting pageContext.nonce to tell Vike to add the nonce tag attribute.
    renderPage({ nonce: 'random-string' })
    
  • Mentioning it in a new short documentation page vike.dev/CSP.

Contribution welcome.

brillout avatar Mar 17 '24 08:03 brillout

Idea: also alow the user to set pageContext.nonce to true and, in that case, Vike will generate a nonce.

@JonWatkins What do you use to generate the nonce? Is Math.random() secure enough?

brillout avatar Mar 17 '24 15:03 brillout

The way that I was generating the nonce was with the crypto module, not sure that Math.random would be enough. But this also ensures that the value changes for every request.

app.use((req: Request, res: Response, next: NextFunction) => {
    res.locals.cspNonce = crypto.randomBytes(16).toString("hex");
    next();
  });

JonWatkins avatar Mar 18 '24 11:03 JonWatkins

Indeed.

:+1: And I guess crypto.randomBytes(16).toString("hex") isn't too slow?

If it's relatively slow, I'd be inclined for going for something faster albeit less secure.

brillout avatar Mar 18 '24 14:03 brillout

I don't have any issues with it in our app. it's such a negligable amount of time, even when the server is under heavy load. The parsing and serialization of the rendered page is more work than generating the nonce.

I think the implementation of the generated nonce might be best left to who ever is implementing it, as other people may have different requirements for it, but having support and docs would be super helpful.

We are using helmet for the handling the CSP headers like this so that it is able to use the res.locals.cspNonce

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      objectSrc: ["'none'"],
      scriptSrc: [
        "'self'",
        (req, res) => `'nonce-${res.locals.cspNonce}'`,
      ],
    },
  },
});

JonWatkins avatar Mar 21 '24 19:03 JonWatkins

Hey all! I am looking to help contribute to Vike, and I started working on this issue. I created a draft PR with some small changes. I wanted to post and see if I am on the right path before I continue forward. @brillout, I see that you proposed to add the nonce property as a boolean to the pageContext for renderPage. Hopefully I had the right idea with this one 😅

Thanks in advance!

branberry avatar May 15 '24 14:05 branberry