Files
HealthLog/src/lib/logging/redact.ts
T
Marc-André Bombeck 70353882eb fix(security): close moodLog SSRF + central error redaction; fix citation drift (#148)
The v1.4.1 audit pass turned up one CRITICAL, two HIGH, and four
medical-citation drift findings. This commit addresses everything
that's safe to land in a hardening release; deeper architectural
fixes (idempotency-race transactional rewrite, encryption-key
fallback gate, refresh-token reuse-detection serialisation, moodLog
secret HMAC lookup column) are tracked for v1.4.2 in
docs/ops/v141-followup-issues.md.

Security
--------

CRITICAL — moodLog SSRF
  src/lib/validations/moodlog.ts now refines the credentials URL
  with isPublicUrl() so an authenticated user cannot store
  http://169.254.169.254/ (cloud metadata) or RFC1918 hosts.
  src/lib/moodlog/sync.ts re-checks isPublicUrl(baseUrl) at the
  fetch site (so legacy rows stored before the credential guard are
  also refused) and switches to redirect: "manual" + 3xx → fail so a
  public host cannot 302 the request to an internal target with the
  user's apiKey on the redirect hop. New unit test
  src/lib/validations/__tests__/moodlog.test.ts asserts the guard
  rejects 169.254.x.x, 10.x, 172.16.x.x, 192.168.x.x, 127.0.0.1, and
  localhost.

HIGH — error-message secret leakage
  New src/lib/logging/redact.ts redacts Bearer tokens (incl. our
  hlk_/hlr_), Telegram bot URLs (bot<digits>:<token>), and
  query-string secrets (?secret=, ?code=, ?token=, ?api_key=). The
  redactor is called from WideEventBuilder.setError() — every error
  reaching Loki is scrubbed once, centrally — and from the
  reportToGlitchtip() call in api-handler.ts so the Glitchtip
  incident UI is also protected. New unit suite covers the four
  patterns plus over-redaction (Bearer's \\S+ greedily consumes
  anything up to the next whitespace, which is the safer side of
  the trade-off).

Truthfulness — citation drift fixes (audit found 0 hallucinations,
4 drift items)
  - messages/{en,de}.json bpClassificationTitle and avg30dEsc:
    "ESC/ESH 2018" → "ESH 2023". User-visible on the dashboard and
    doctor-report PDF. The numeric thresholds are unchanged from
    2018; the 2023 update is ESH-only since ESC withdrew from
    joint authoring.
  - src/lib/analytics/classifications.ts: section header,
    getBpTargetsByAge() docblock, and inline comment updated to
    cite ESH 2023 with the same "numerically unchanged" caveat.
  - src/app/api/insights/targets/route.ts:375: source label "WHO"
    → "Saint-Maurice JAMA 2020". The AI-prompt anti-pattern guard
    explicitly forbids citing WHO for a step number; this surface
    was the last "WHO" mislabel in the codebase.
  - Saint-Maurice "mortality plateau 8000-12000" attribution
    softened to "continued dose-response benefit through ~12,000
    steps/day, not a plateau" in src/lib/medical-citations.ts,
    src/lib/analytics/effective-range.ts,
    src/lib/analytics/classifications.ts, and both AI prompts. The
    original JAMA 2020 paper shows continuing benefit (HR 0.49 at
    8k vs 4k, HR 0.35 at 12k); the plateau-shaped finding belongs
    to Paluch 2022 Lancet PH (PMID 35247352), not Saint-Maurice.

Quality gates
  pnpm typecheck         clean
  pnpm test              669 / 669 (was 658; +11 from new suites)
  pnpm test:integration  10 / 10 (untouched)
  pnpm lint              0 errors
2026-05-08 16:30:37 +02:00

36 lines
1.5 KiB
TypeScript

/**
* Redact secrets from strings before they leave the process boundary
* (Loki, Glitchtip, console). Applied in `WideEventBuilder.setError()`
* and `reportToGlitchtip()` so every error reaching observability is
* scrubbed once, centrally.
*
* Patterns:
* - `Bearer <token>` — generic API tokens incl. our `hlk_` /
* `hlr_` formats and external OAuth bearers (Withings, AI providers).
* - `bot<digits>:<token>` — Telegram bot URLs embed the token in the
* path: `https://api.telegram.org/bot1234567:ABC-…/sendMessage`.
* If `fetch()` rejects with a `cause` that surfaces the URL (some
* Node runtimes), the token would land in error reports.
* - `?secret=…` and `?code=…` — query-string leaks for legacy
* Withings webhooks and OAuth callbacks (already scrubbed at the
* URL level in `reportToGlitchtip`, but error.message can carry
* the URL too).
*
* The substitution is intentionally generic ([REDACTED]) — we don't
* want partial revelation of token entropy.
*/
export function redactSecrets(input: string): string {
return input
.replace(/Bearer\s+\S+/gi, "Bearer [REDACTED]")
.replace(/bot\d+:[A-Za-z0-9_-]+/g, "bot[REDACTED]")
.replace(
/([?&])(secret|code|token|api[_-]?key)=[^&\s]+/gi,
"$1$2=[REDACTED]",
);
}
/** Apply `redactSecrets` to a possibly-undefined string. */
export function redactOptional(s: string | undefined): string | undefined {
return s === undefined ? undefined : redactSecrets(s);
}