]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
feat(ui-server): add opt-in HSTS response header (#1980) (#2039)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Mon, 20 Jul 2026 10:52:13 +0000 (12:52 +0200)
committerGitHub <noreply@github.com>
Mon, 20 Jul 2026 10:52:13 +0000 (12:52 +0200)
commit74fae39fd81f138dd9b68c31157018b7813b527c
treea8f261ab45b6bb8e6ac2b62be95bccb069296a13
parent503d71c848d5e5749efd21c6b1b4b85ef1f3602e
feat(ui-server): add opt-in HSTS response header (#1980) (#2039)

Add an opt-in `uiServer.securityHeaders.strictTransportSecurity` config
knob (`string | false`) that, when set to a non-empty string, emits a
`Strict-Transport-Security` (HSTS) response header on the UI server.

Emission is gated on secure transport per RFC 6797 §7.2 (which forbids
sending HSTS over non-secure transport): the header is emitted only when
the response travels over direct TLS (`req.socket.encrypted`) or a trusted
reverse proxy that forwarded a secure protocol (`https`/`wss`), resolved by
a new `isRequestEffectivelySecure` predicate that reuses the access-policy
trusted-proxy/forwarded-protocol machinery. Over plaintext (e.g. loopback
development) the header is omitted.

The header is spread — via `getSecurityHeaders(isSecure)` and, for the
async HTTP success path, a per-uuid `secureResponses` map mirroring
`acceptsGzip` — across every UI-server HTTP response path that this code
writes: shared denials (`renderDenial`, now request-aware), HTTP success
responses (gzip and non-gzip), WebSocket upgrade rejections, and the
`/metrics` success and error responses, on the http/ws/mcp transports. MCP
JSON-RPC success bodies are written by the MCP SDK transport and are not
covered.

Mirrors the `metrics` opt-in sub-schema: one `.strict()` leaf schema, the
derived `z.infer` type, and header spreads. No new dependency; no OCPP
PDU/message-format change (HTTP transport header only).

The header is absent by default (knob unset, `false`, or empty string) and
over non-secure transport, so the default response behavior is unchanged.
Recommended production value behind a TLS-terminating reverse proxy or
native TLS: "max-age=31536000; includeSubDomains".

Closes only slice C of #1980; the identity-aware proxy mode, local-interface
spoofing guard, security audit CLI, and dangerously* naming slices remain open.
15 files changed:
README.md
cspell.config.yaml
src/charging-station/ui-server/AbstractUIServer.ts
src/charging-station/ui-server/UIHttpServer.ts
src/charging-station/ui-server/UIMCPServer.ts
src/charging-station/ui-server/UIServerAccessPolicy.ts
src/charging-station/ui-server/UIWebSocketServer.ts
src/types/ConfigurationData.ts
src/utils/ConfigurationSchema.ts
src/utils/index.ts
tests/charging-station/ui-server/UIHttpServer.test.ts
tests/charging-station/ui-server/UIMCPServer.test.ts
tests/charging-station/ui-server/UIServerAccessPolicy.test.ts
tests/charging-station/ui-server/UIWebSocketServer.test.ts
tests/utils/ConfigurationSchema.test.ts