fix(security): explicitly disable upgrade-insecure-requests via Helmet 8 API

Helmet 8 merges custom directives with its built-in defaults, which
include upgrade-insecure-requests. Simply omitting the directive from
the custom object (PR #59) was insufficient — Helmet silently re-adds
it from defaults. Setting upgradeInsecureRequests: null is the correct
Helmet 8 API to remove a default directive.

This was the root cause of the persistent blank page on plain-HTTP
self-hosted deployments: the directive tells browsers to upgrade all
HTTP sub-resource fetches to HTTPS, producing ERR_SSL_PROTOCOL_ERROR
on every JS/CSS asset.
This commit is contained in:
SaelixCode
2026-03-22 17:26:08 -04:00
parent 278aa2298f
commit 50df5b3c02
2 changed files with 12 additions and 5 deletions
+1
View File
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- **Blank page on HTTP deployments (root cause — Helmet 8 default CSP):** Helmet 8 merges custom `directives` with its built-in defaults, which include `upgrade-insecure-requests`. The previous fix (PR #59) omitted the directive from the custom object, but Helmet silently re-added it from defaults. The correct fix is `upgradeInsecureRequests: null`, which is the Helmet 8 API for explicitly removing a default directive.
- **Login loop caused by remote node auth failure:** When a configured remote node had an expired or invalid API token, every proxied request to that node returned 401. Both `apiFetch` and `fetchForNode` treated any 401 as a user session failure and dispatched `sencho-unauthorized`, immediately logging the user out and sending them back to the login page — even after a successful login. Fixed by adding a `proxyRes` handler that stamps all remote-proxied responses with `x-sencho-proxy: 1`; the frontend now only fires `sencho-unauthorized` when this header is absent (i.e., it's a genuine local session failure).
- **Missing `authMiddleware` on notifications endpoints:** `GET /api/notifications`, `POST /api/notifications/read`, `DELETE /api/notifications/:id`, `DELETE /api/notifications`, and `POST /api/notifications/test` were all missing `authMiddleware`, violating the default-deny policy. Added to all five.
- **CSP `workerSrc` missing (Monaco editor workers):** The Content Security Policy had no explicit `worker-src` directive, which in practice relied on `default-src 'self'`. Monaco editor creates Web Workers via `blob:` URLs for language services; these were silently failing. Added `worker-src 'self' blob:`.
+11 -5
View File
@@ -68,10 +68,12 @@ const getCookieOptions = (req: Request) => ({
// crossOriginEmbedderPolicy: disabled — Monaco editor workers lack COEP headers.
// hsts: disabled — HSTS must only be set when the app is served over HTTPS.
// Enabling it over HTTP permanently breaks browser access for 1 year.
// contentSecurityPolicy.upgrade-insecure-requests: removed — this directive
// tells browsers to silently upgrade all HTTP sub-resource fetches to HTTPS.
// On a plain-HTTP self-hosted deployment (the common case) this causes every
// JS/CSS asset to fail with ERR_SSL_PROTOCOL_ERROR, producing a blank page.
// contentSecurityPolicy.upgradeInsecureRequests: explicitly set to null.
// Helmet 8 merges custom directives with its defaults, which include this
// directive. It tells browsers to silently upgrade all HTTP sub-resource fetches
// to HTTPS. On a plain-HTTP self-hosted deployment (the common case) this causes
// every JS/CSS asset to fail with ERR_SSL_PROTOCOL_ERROR, producing a blank page.
// Setting null is the Helmet 8 API to remove a default directive.
app.use(helmet({
crossOriginEmbedderPolicy: false,
hsts: false,
@@ -93,7 +95,11 @@ app.use(helmet({
// worker-src: Monaco editor creates Web Workers via blob: URLs for language
// services (syntax highlighting, intellisense). Without blob: they silently fail.
workerSrc: ["'self'", 'blob:'],
// 'upgrade-insecure-requests' is intentionally absent — see comment above.
// Helmet 8 merges custom directives with its defaults, which include
// upgrade-insecure-requests. Setting it to null explicitly removes it.
// On plain-HTTP self-hosted deployments (the common case) this directive
// causes every JS/CSS asset to fail with ERR_SSL_PROTOCOL_ERROR → blank page.
upgradeInsecureRequests: null,
},
},
}));