Commit Graph
4 Commits
Author SHA1 Message Date
Hermes 6b7ca4b3e9 [grade=A urn:ump:qszwspay7vhx7wcpqekkkidni3ehhgidvze5qk2y22fvg2wqz3ja] fix(DC-170): reliability batch — hangs, atomicity, fail-closed, SW (audit batch 6)
8 reliability fixes across 8 files (AUDIT-2026-10-27.md findings
9/11/12/13/14/16/20 + calendar credential item):

- dns.js check-update/update HANGS: bare returns + empty else/catch
  branches left requests hanging until client timeout; every path now
  answers (502 with message); upstream JSON.parse wrapped (non-JSON ->
  502)
- calendar.js ICS fetch timeout actually applies: fetchT ignores
  opts.timeout (undici) — was running at 5s default, not the intended
  15s; now timeoutMs third arg
- calendar.js PUT /sources: updating pass no longer nulls the stored
  token (and vice versa) — existing credential record loaded, only the
  provided field overwritten
- state-manager.js ATOMIC WRITES: _atomicWrite = tmp file + fs.rename
  (POSIX-atomic); write()/update() no longer truncate-then-write the
  live file (crash mid-write corrupted services.json/config.json with
  no recovery path). Test mock extended to the tmp+rename contract
- dashboard-ws.js auth fallback FAIL-CLOSED: missing authVerifier used
  to accept any non-empty session cookie (DC-061 footgun on unwired
  boot paths); now rejects + loud CRITICAL log
- middleware.js tailscale gate: new tailscale.failClosed config (default
  off = unchanged); when set, outages/errors deny (503) instead of
  silently disabling the gate; fail-open path now logs the exact key
- sw.js: offline fallback actually works ((await match) || (await
  match) — old Promise||Promise was always truthy); only 2xx cached
  (5xx pages were stored and served as the app shell); install is
  per-URL tolerant (one bad asset kept the stale SW alive forever)
- config/settings.js GET /config (public): corrupt config.json degrades
  to {setupComplete:false, corrupt:true} instead of 500 on every load

SCOPE CORRECTION: audit finding #10 (7 reversed-arg errorResponse
sites) was a FALSE POSITIVE — those files alias message-first error();
see lesson #14. Full suite 173/173 suites, 3740/3740 tests.
2026-10-29 01:46:13 -07:00
Krystie 678a0160c4 [glm-grade=A] fix(websocket): preserve default-export compat for createDashboardWS
Pre-fix WIP changed module.exports to a named object {createDashboardWS,
parseCookieHeader}. server.js still uses  (default-import style) so require() returned an object and
the call site failed at boot with TypeError: createDashboardWS is not a
function. Container crashed on every start.sh until fixed.

Both import shapes must work:
  const createDashboardWS = require('...');          // default
  const { createDashboardWS } = require('...');      // named
  const { createCookieHeader } = require('...');

module.exports = createDashboardWS keeps the default callable shape;
the appended properties carry the named exports for the test file.

Discovered by live-verify after deploy — TypeError visible in
docker logs dashcaddy-api --since 60s. GLM-5.3 judge missed the import
site check (only grep'd source, not server.js require line) — graded A
but missed this contract regression. Round-2 fix shipped same tick.
2026-08-18 08:28:24 -07:00
Krystie 30d5fdbb2c [glm-grade=A] fix(websocket): HMAC-verify dashboard WS auth + listener isolation (DC-061)
Pre-fix: /api/v1/ws checked cookies.includes('dashcaddy_session') — substring
match, bypassable with Cookie: dashcaddy_session=garbage. Production also
accepted any 11+ char ?token= query string. Both let any attacker subscribe
to all real-time event streams (status-change, incident, cert-expiring,
auto-restart, dependency-restart, update-available, drift-detected, etc).

Fix (3 files, +404/-81):

(1) server.js:80-99 wires ctx.session.isValid (HMAC-verifying isSessionValid
from middleware.js:265-279) into deps.authVerifier so production goes
through the same signed-cookie verifier as the REST routes.

(2) dashboard-ws.js:
  - New parseCookieHeader helper (exported for test coverage)
  - authVerifier injection: deps.authVerifier default is a presence-only
    fallback for unusual boot paths; production wires the HMAC verifier.
  - Upgrade handler replaces substring check with authVerifier(request).
    401 includes Connection: close so browsers don't retry. Logs WS upgrade
    rejections at WARN with ip + path.
  - Removes ?token= query param bypass entirely (any random 11+ char token
    previously granted production access).
  - 16 KB message size cap defense-in-depth in the message handler.

(3) close() now detaches ONLY the listeners dashboard-ws attached via the
new attachListener() helper. The previous code called
resourceMonitor.removeAllListeners() (and same for healthChecker /
updateManager / sslMonitor / dnsPropagationChecker), which silently killed
the SSE route's listeners on the same shared emitters every time close()
ran (hot reload, graceful restart). The new test proves the SSE listener
survives dashboard-ws.close() and the resourceMonitor still emits to it.

Tests (+273/-33, 24/24 pass, full suite 2018/2018, +16 net):
  - 6 auth gate probes: no cookie, empty session cookie, unrelated cookie,
    ?token= bypass rejected, token+empty-cookie combo rejected, valid
    cookie grants 101
  - 2 listener-isolation: close() detaches only OUR listeners; close() is
    idempotent
  - 8 parseCookieHeader unit tests (undefined, empty, single, multi,
    whitespace, HMAC-shaped value preservation, malformed pair, empty name)
  - Existing DC-076 tests updated to send Cookie header

Refs: codex-as-judge SKILL.md threat model — WS endpoint bypassed the
Express middleware chain, so the global totpAuthMiddleware never ran
on the upgrade request. Auth must be re-asserted at the upgrade handler.
2026-08-18 08:22:29 -07:00
Krystie 503de258b8 [grade=pending] QA sprint: commit 103 at-risk files from multi-agent sprint work
Committed by Hermes autonomous QA sprint 2026-08-13.
These files were modified during the Aug 12 sprint but never committed.
2026-08-12 17:34:10 -07:00