mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
feat(share-links): scrub the share token from logs and harden the /c edge
Register the `/c/` prefix in `PATH_SECRET_PATHS` so the raw `hls_` clinician share token, which rides as the trailing path segment, is rewritten to [REDACTED] in `http.path` / `http.route` before any Wide Event or GlitchTip report leaves the process — the same posture as the Withings webhook path secret. The resolve path never logs, throws, or annotates the raw token. Defend the share surface at the edge irrespective of what the RSC emits: the proxy now sets `Cache-Control: no-store`, `X-Robots-Tag: noindex, nofollow`, and `Referrer-Policy: no-referrer` on `/c/` responses, so no downstream proxy caches a scoped record, no crawler indexes the token, and the token-bearing URL is not leaked in an outbound `Referer`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5917bba61c
commit
f4629a17d6
3 files changed
+58
-4
No files matched your search
@@ -195,6 +195,35 @@ describe("redactSecrets", () => {
|
||||
"/api/withings/auth/callback",
|
||||
);
|
||||
});
|
||||
|
||||
// v1.11.0 (Epic C, C6) — the raw `hls_` clinician share token rides as
|
||||
// the trailing path segment of `/c/<token>`. It must be scrubbed from
|
||||
// `http.path` / `http.route` before the Wide Event leaves the process.
|
||||
it("registers the clinician share-view prefix in PATH_SECRET_PATHS", () => {
|
||||
expect(PATH_SECRET_PATHS.map((entry) => entry.prefix)).toContain("/c/");
|
||||
});
|
||||
|
||||
it("redacts the share token in the `/c/<token>` path", () => {
|
||||
expect(
|
||||
redactSecrets(
|
||||
"/c/hls_0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
),
|
||||
).toBe("/c/[REDACTED]");
|
||||
});
|
||||
|
||||
it("redacts the share token in an absolute URL form", () => {
|
||||
expect(
|
||||
redactSecrets(
|
||||
"https://app.healthlog.dev/c/hls_0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
),
|
||||
).toBe("https://app.healthlog.dev/c/[REDACTED]");
|
||||
});
|
||||
|
||||
it("redacts the share token even with a trailing query string", () => {
|
||||
expect(redactSecrets("/c/hls_deadbeef?print=1")).toBe(
|
||||
"/c/[REDACTED]?print=1",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
* log them on purpose, but a misconfigured client error or a
|
||||
* dump of the request body could carry one. Scrub before egress.
|
||||
* - path-segment secrets — routes like `/api/withings/webhook/<secret>`
|
||||
* carry the shared secret as a positional path segment. The Wide
|
||||
* Event `http.path` and `http.route` fields would otherwise leak
|
||||
* the secret into stdout, the in-memory ring buffer, and Loki.
|
||||
* See `PATH_SECRET_PATHS`.
|
||||
* and the clinician share view `/c/<hls_…>` carry a bearer secret as
|
||||
* a positional path segment. The Wide Event `http.path` and
|
||||
* `http.route` fields would otherwise leak the secret into stdout,
|
||||
* the in-memory ring buffer, and Loki. See `PATH_SECRET_PATHS`.
|
||||
*
|
||||
* The substitution is intentionally generic ([REDACTED]) — we don't
|
||||
* want partial revelation of token entropy.
|
||||
@@ -42,6 +42,11 @@ export const PATH_SECRET_PATHS: ReadonlyArray<{ prefix: string }> = [
|
||||
// the trailing path segment (v1.4.25 W17a). Without this rule the
|
||||
// secret lands in every Wide Event's `http.path` / `http.route`.
|
||||
{ prefix: "/api/withings/webhook/" },
|
||||
// v1.11.0 — clinician share view (Epic C). The raw `hls_` share token
|
||||
// rides as the trailing path segment of `/c/<token>`; it is a bearer
|
||||
// credential for the share surface, so scrub it from `http.path` /
|
||||
// `http.route` the same way the Withings webhook secret is scrubbed.
|
||||
{ prefix: "/c/" },
|
||||
];
|
||||
|
||||
function redactPathSegments(input: string): string {
|
||||
|
||||
@@ -237,6 +237,26 @@ export function proxy(request: NextRequest) {
|
||||
"Permissions-Policy",
|
||||
"camera=(), microphone=(), geolocation=()",
|
||||
);
|
||||
|
||||
// v1.11.0 (Epic C, C6) — the public clinician share view at `/c/<token>`
|
||||
// is a scoped health record authenticated by an unguessable bearer token
|
||||
// in the path. Defend it at the edge regardless of what the RSC emits:
|
||||
// - `Cache-Control: no-store` so no shared proxy / CDN ever retains a
|
||||
// scoped record (the page is `force-dynamic`, but that governs Next's
|
||||
// cache, not a downstream intermediary).
|
||||
// - `X-Robots-Tag: noindex, nofollow` as the header peer of the page's
|
||||
// `robots` meta — a crawler that never parses the document still obeys
|
||||
// the header, and the token must never reach a search index.
|
||||
// - `Referrer-Policy: no-referrer` so the token-bearing URL is not
|
||||
// leaked in the `Referer` of any outbound navigation from the page.
|
||||
if (pathname.startsWith("/c/")) {
|
||||
response.headers.set(
|
||||
"Cache-Control",
|
||||
"no-store, no-cache, must-revalidate",
|
||||
);
|
||||
response.headers.set("X-Robots-Tag", "noindex, nofollow");
|
||||
response.headers.set("Referrer-Policy", "no-referrer");
|
||||
}
|
||||
// COOP isolates this BrowsingContextGroup from cross-origin popups,
|
||||
// closing the Spectre-class side-channel surface a stray
|
||||
// `window.opener` reference would otherwise carry. CORP narrows the
|
||||
|
||||
Reference in new issue
Block a user