helmetjs / helmet

Help secure Express apps with various HTTP headers
https://helmetjs.github.io/
MIT License
10.24k stars 369 forks source link

RFE: Static pre-computed headers #430

Closed glensc closed 1 year ago

glensc commented 1 year ago

I have an express app, which does 301 redirects or 404 responses. so the headers that helmet() adds are always static.

Perhaps it could be possible to pre-compute the headers and in middleware just apply the pre-computed headers. as there's no real need to validate the options and apply parsing logic for each request that comes through this middleware.

since this project also removes some headers (x-powered-by) the pre-computed results could just be two variables:

glensc commented 1 year ago

In my test app the headers diff before and after helmet are:

-X-Powered-By: Express
+Content-Security-Policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests
+Cross-Origin-Opener-Policy: same-origin
+Cross-Origin-Resource-Policy: same-origin
+Origin-Agent-Cluster: ?1
+Referrer-Policy: no-referrer
+Strict-Transport-Security: max-age=15552000; includeSubDomains
+X-Content-Type-Options: nosniff
+X-DNS-Prefetch-Control: off
+X-Download-Options: noopen
+X-Frame-Options: SAMEORIGIN
+X-Permitted-Cross-Domain-Policies: none
+X-XSS-Protection: 0
glensc commented 1 year ago

I've created such middleware:

import { IncomingMessage, ServerResponse } from "http";

import { Express } from "express";

type T = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;

export const helmet = (app: Express): T => {
  app.disable("x-powered-by");

  const headers = {
    "Content-Security-Policy": [
      "default-src 'self'",
      "base-uri 'self'",
      "font-src 'self' https: data:",
      "form-action 'self'",
      "frame-ancestors 'self'",
      "img-src 'self' data:",
      "object-src 'none'",
      "script-src 'self'",
      "script-src-attr 'none'",
      "style-src 'self' https: 'unsafe-inline'",
      "upgrade-insecure-requests",
    ].join(";"),
    "Cross-Origin-Opener-Policy": "same-origin",
    "Cross-Origin-Resource-Policy": "same-origin",
    "Origin-Agent-Cluster": "?1",
    "Referrer-Policy": "no-referrer",
    "Strict-Transport-Security": "max-age=15552000; includeSubDomains",
    "X-Content-Type-Options": "nosniff",
    "X-DNS-Prefetch-Control": "off",
    "X-Download-Options": "noopen",
    "X-Frame-Options": "SAMEORIGIN",
    "X-Permitted-Cross-Domain-Policies": "none",
    "X-XSS-Protection": "0",
  };

  return (req: IncomingMessage, res: ServerResponse, next: () => void): void => {
    for (const [name, value] of Object.entries(headers)) {
      res.setHeader(name, value);
    }

    next();
  };
};

can be used as:

app.use(helmet(app));
EvanHahn commented 1 year ago

I built a simple benchmarking app to compare these two approaches.

The Helmet-based approach looks like this:

app.use(helmet());

The precomputed approach looks like this:

const HEADERS = { /* ... */ };

app.disable("x-powered-by");

app.use((req, res, next) => {
  res.set(HEADERS);
  next();
});

The precomputed version is faster. On my machine, I could send 289K requests per minute, or about 4.8K requests per second.

Compare that to the Helmet-based version, which did 268K requests per minute, or about 4.5K requests per second. That's about 7% slower.

I don't think that's significant enough to add new things to Helmet, but I'll think more about this and get back to you.

EvanHahn commented 1 year ago

After some consideration, I don't think I plan to make any code changes to Helmet.

However, I do think this is worth documenting. I added a page to the documentation showing how to set these headers yourself. (I also published it to my blog, hoping for slightly greater visibility.)

Thanks for opening this issue.

glensc commented 1 year ago

res.set() is alias to .header():

so perhaps:

    res.header(headers);
EvanHahn commented 1 year ago

Sure, that works too!