vikejs / vike

🔨 Flexible, lean, community-driven, dependable, fast Vite-based frontend framework.
https://vike.dev
MIT License
4.38k stars 350 forks source link

CSP nonce support #1554

Open JonWatkins opened 8 months ago

JonWatkins commented 8 months ago

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.

brillout commented 8 months ago

I agree that'd be nice.

I think the following would be a simple solution.

Contribution welcome.

brillout commented 8 months ago

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?

JonWatkins commented 8 months ago

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();
  });
brillout commented 8 months ago

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.

JonWatkins commented 8 months ago

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}'`,
      ],
    },
  },
});
branberry commented 6 months ago

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!