mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(logging): redact http.path + http.route in WideEventBuilder.setHttp
Closes the WITHINGS_WEBHOOK_SECRET leak (sec-C1): the api-handler seeds http.path and http.route from url.pathname, which carries the shared secret for /api/withings/webhook/<secret> requests. Routing both fields through redactSecrets() at setHttp() time keeps the secret out of stdout, the in-memory ring buffer, and Loki egress.
This commit is contained in:
1 parent
8da6a4ae69
commit
44e8229ff3
2 files changed
+126
-1
No files matched your search
@@ -0,0 +1,112 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { WideEventBuilder } from "../event-builder";
|
||||
|
||||
describe("WideEventBuilder.setHttp", () => {
|
||||
it("stores plain http metadata as-is", () => {
|
||||
const evt = new WideEventBuilder("http");
|
||||
evt.setHttp({
|
||||
method: "GET",
|
||||
path: "/api/health",
|
||||
route: "/api/health",
|
||||
status: 200,
|
||||
});
|
||||
const json = evt.toJSON();
|
||||
expect(json.http).toMatchObject({
|
||||
method: "GET",
|
||||
path: "/api/health",
|
||||
route: "/api/health",
|
||||
status: 200,
|
||||
});
|
||||
});
|
||||
|
||||
// Fix-J (v1.4.25 W21, sec-C1): WITHINGS_WEBHOOK_SECRET lands in
|
||||
// http.path on every request without this scrub. Verify the redaction
|
||||
// is applied at setHttp time so the secret never reaches stdout, the
|
||||
// in-memory ring buffer, or Loki.
|
||||
it("redacts Withings webhook secret from http.path and http.route", () => {
|
||||
const evt = new WideEventBuilder("http");
|
||||
evt.setHttp({
|
||||
method: "POST",
|
||||
path: "/api/withings/webhook/test-secret",
|
||||
route: "/api/withings/webhook/test-secret",
|
||||
status: 200,
|
||||
});
|
||||
const http = evt.toJSON().http;
|
||||
expect(http?.path).toBe("/api/withings/webhook/[REDACTED]");
|
||||
expect(http?.route).toBe("/api/withings/webhook/[REDACTED]");
|
||||
});
|
||||
|
||||
it("preserves trailing path segments after the redacted secret", () => {
|
||||
const evt = new WideEventBuilder("http");
|
||||
evt.setHttp({
|
||||
method: "GET",
|
||||
path: "/api/withings/webhook/sup3r-secr3t/verify",
|
||||
route: "/api/withings/webhook/[token]/verify",
|
||||
status: 200,
|
||||
});
|
||||
const http = evt.toJSON().http;
|
||||
expect(http?.path).toBe("/api/withings/webhook/[REDACTED]/verify");
|
||||
// The Next.js route template `[token]` does not match the
|
||||
// path-segment rule's `<segment>` shape boundary (the brackets are
|
||||
// treated as a regular segment), so it is still redacted; either
|
||||
// shape is acceptable since the goal is to never leak the secret.
|
||||
expect(http?.route).toBe("/api/withings/webhook/[REDACTED]/verify");
|
||||
});
|
||||
|
||||
it("does not over-redact unrelated paths", () => {
|
||||
const evt = new WideEventBuilder("http");
|
||||
evt.setHttp({
|
||||
method: "GET",
|
||||
path: "/api/measurements/123",
|
||||
route: "/api/measurements/[id]",
|
||||
status: 200,
|
||||
});
|
||||
const http = evt.toJSON().http;
|
||||
expect(http?.path).toBe("/api/measurements/123");
|
||||
expect(http?.route).toBe("/api/measurements/[id]");
|
||||
});
|
||||
|
||||
it("still scrubs query-string secrets via the existing rule", () => {
|
||||
const evt = new WideEventBuilder("http");
|
||||
evt.setHttp({
|
||||
method: "POST",
|
||||
path: "/api/withings/webhook?secret=abc123",
|
||||
route: "/api/withings/webhook",
|
||||
status: 200,
|
||||
});
|
||||
const http = evt.toJSON().http;
|
||||
expect(http?.path).toBe("/api/withings/webhook?secret=[REDACTED]");
|
||||
expect(http?.route).toBe("/api/withings/webhook");
|
||||
});
|
||||
|
||||
it("preserves other http fields verbatim", () => {
|
||||
const evt = new WideEventBuilder("http");
|
||||
evt.setHttp({
|
||||
method: "POST",
|
||||
path: "/api/withings/webhook/leak",
|
||||
route: "/api/withings/webhook/leak",
|
||||
status: 200,
|
||||
user_agent: "WithingsClient/1.0",
|
||||
ip: "10.0.0.1",
|
||||
content_length: 42,
|
||||
response_size: 17,
|
||||
});
|
||||
const http = evt.toJSON().http;
|
||||
expect(http).toMatchObject({
|
||||
method: "POST",
|
||||
path: "/api/withings/webhook/[REDACTED]",
|
||||
route: "/api/withings/webhook/[REDACTED]",
|
||||
status: 200,
|
||||
user_agent: "WithingsClient/1.0",
|
||||
ip: "10.0.0.1",
|
||||
content_length: 42,
|
||||
response_size: 17,
|
||||
});
|
||||
});
|
||||
|
||||
it("handles an undefined http payload without throwing", () => {
|
||||
const evt = new WideEventBuilder("http");
|
||||
evt.setHttp(undefined);
|
||||
expect(evt.toJSON().http).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -38,7 +38,20 @@ export class WideEventBuilder {
|
||||
}
|
||||
|
||||
setHttp(http: WideEvent["http"]): this {
|
||||
this.event.http = http;
|
||||
if (http) {
|
||||
// Fix-J (v1.4.25 W21): the Withings webhook ships its shared
|
||||
// secret as a path segment, so `path` and `route` carry the
|
||||
// secret unless we scrub on the way in. `redactSecrets` is the
|
||||
// single egress-redaction surface (see redact.ts for the rules
|
||||
// and the PATH_SECRET_PATHS registry).
|
||||
this.event.http = {
|
||||
...http,
|
||||
path: redactSecrets(http.path),
|
||||
route: redactSecrets(http.route),
|
||||
};
|
||||
} else {
|
||||
this.event.http = http;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user