Helmet 配置方法与 Express 5 实战

1. 安装与最小使用

npm install helmet

Helmet 自带 TypeScript 类型声明,现代版本通常不需要额外安装 @types/helmet

import express from "express";
import helmet from "helmet";

const app = express();
const port = 8080;

app.use(helmet());

app.get("/health", (_req, res) => {
  res.json({ ok: true });
});

app.listen(port, () => {
  console.log(`server listening on http://127.0.0.1:${port}`);
});

本项目用 TypeScript 编译为 CommonJS,但源码仍可使用 import。若项目没有开启 esModuleInterop,默认导入是否可用取决于当前 TypeScript 配置和依赖导出方式;应以项目现有 tsconfig.json 的类型检查结果为准。

验证:

curl -i http://127.0.0.1:8080/health

2. 配置对象

可以保留有学习价值的显式类型:

import helmet, { type HelmetOptions } from "helmet";

const helmetOptions: HelmetOptions = {
  crossOriginResourcePolicy: {
    policy: "same-site",
  },
  referrerPolicy: {
    policy: "strict-origin-when-cross-origin",
  },
};

app.use(helmet(helmetOptions));

显式类型能让编辑器提示可用字段并尽早发现拼写错误。若类型名随依赖版本变化,以安装版本导出的类型为准;也可以让 TypeScript 从 helmet({...}) 参数推断。

3. 关闭单项策略

如果确认某项默认策略与业务冲突,可以只关闭这一项:

app.use(
  helmet({
    contentSecurityPolicy: false,
  }),
);

这不应成为“页面报错就关闭 Helmet”的固定做法。正确顺序是:

  1. 查看浏览器 Console 的具体违规;
  2. 确认哪个响应携带策略;
  3. 补充最小必要来源或调整单项策略;
  4. 记录关闭原因并安排复核。

4. 配置 CSP

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "https://cdn.example.com"],
        imgSrc: ["'self'", "data:", "https://images.example.com"],
        connectSrc: ["'self'", "https://api.example.com"],
        objectSrc: ["'none'"],
        frameAncestors: ["'none'"],
      },
    },
  }),
);

注意字符串中的单引号是 CSP 语法的一部分,"'self'" 与普通字符串 "self" 不相同。

Nonce 示例

有内联脚本需求时,不要轻率加入 'unsafe-inline'。可以为每个 HTML 响应生成随机 nonce,并在 CSP 与 <script nonce="..."> 中使用同一个值。

import { randomBytes } from "node:crypto";
import type { NextFunction, Request, Response } from "express";

function createCspNonce(
  _req: Request,
  res: Response,
  next: NextFunction,
): void {
  res.locals.cspNonce = randomBytes(16).toString("base64");
  next();
}

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

res.locals 随当前响应生命周期使用;请求结束且没有其他长期引用时,数据即可被垃圾回收。模板渲染时还要把相同 nonce 放到对应脚本标签。

5. 开发环境处理 upgrade-insecure-requests

本地使用 HTTP 时,CSP 的 upgrade-insecure-requests 可能让某些浏览器把本地资源升级为 HTTPS。可以按环境调整 directive:

const isDevelopment = process.env.NODE_ENV === "development";

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        "upgrade-insecure-requests": isDevelopment ? null : [],
      },
    },
  }),
);

null 的具体配置语义应与当前 Helmet 版本文档和类型声明核对。更重要的是测试环境要尽量接近生产 HTTPS,避免上线时才第一次执行真实策略。

6. 单独调用某个 Helmet 中间件

Helmet 暴露了单项中间件,适合精确控制:

app.use(helmet.noSniff());
app.use(helmet.frameguard({ action: "sameorigin" }));
app.use(
  helmet.referrerPolicy({
    policy: "strict-origin-when-cross-origin",
  }),
);

通常优先使用 helmet() 建立完整基线,再按业务调整;只零散启用两三个 Header 容易遗漏保护。

7. 仅对部分路由应用

app.use("/preview", helmet());

app.get("/api/health", (_req, res) => {
  res.json({ ok: true });
});

也可以给 HTML 预览和 JSON API 使用不同策略:

const apiHelmet = helmet({
  contentSecurityPolicy: false,
});

const previewHelmet = helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'none'"],
      objectSrc: ["'none'"],
      frameAncestors: ["'none'"],
    },
  },
});

app.use("/api", apiHelmet);
app.use("/preview", previewHelmet);

这种拆分比“所有响应一套 CSP”更符合不同内容的风险,但不要造成配置散落。建议集中在一个安全配置模块,并说明每项差异。

8. 中间件顺序

一个易理解的基本顺序:

app.use(requestIdMiddleware);
app.use(helmet());
app.use(corsMiddleware);
app.use(express.json());
app.use(authenticationMiddleware);
app.use(apiRouter);
app.use(notFoundHandler);
app.use(errorHandler);

Helmet 和 CORS 较早执行,能让正常、404 和业务错误响应都带上相应 Header。若某个中间件在它们之前直接结束响应,那份响应不会自动得到后面的 Header。

不要多次调用 helmet() 后期待配置自动“合并”。多个中间件可能覆盖或重复 Header,应明确最终责任。

9. HSTS 在 Nginx HTTPS 架构中的选择

当公网 HTTPS 在 Nginx 终止、Express 只监听 127.0.0.1:8080 时,HSTS 通常由 Nginx 设置更清晰。此时可以在 Express 中关闭该项:

app.use(
  helmet({
    strictTransportSecurity: false,
  }),
);

但关闭的前提是确认所有公网 HTTPS 响应(包括错误页)都由 Nginx正确添加 HSTS。不要让两层都以不同 max-ageincludeSubDomains 设置。

10. 升级和测试建议

至少验证这些响应:

升级 Helmet 后运行自动化测试,对关键 Header 做断言,但只断言业务真正依赖的策略,避免把所有默认字符串写死导致正常升级困难。