File size: 3,137 Bytes
34810d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/** Canonical debug-proxy capture header redaction. */
import { Headers as UndiciHeaders } from "undici";
import { afterEach, describe, expect, it } from "vitest";
import { registerSecretValueForRedaction } from "../logging/secret-redaction-registry.js";
import { resetSecretRedactionRegistryForTest } from "../logging/secret-redaction-registry.test-support.js";
import { redactedCaptureHeaders } from "./header-redaction.js";

afterEach(() => {
  resetSecretRedactionRegistryForTest();
});

describe("redactedCaptureHeaders", () => {
  it("redacts credential-bearing header names regardless of case", () => {
    const redacted = redactedCaptureHeaders({
      Authorization: "Bearer live-token",
      COOKIE: "session=abc",
      "X-Api-Key": "sk-live",
      "content-type": "application/json",
    });
    expect(redacted).toEqual({
      Authorization: "[REDACTED]",
      COOKIE: "[REDACTED]",
      "X-Api-Key": "[REDACTED]",
      "content-type": "application/json",
    });
  });

  it("redacts a registered secret pasted into an otherwise innocuous header", () => {
    // The name check alone would pass this through; value redaction is what
    // keeps a leaked token out of the capture.
    registerSecretValueForRedaction("super-secret-value");
    const redacted = redactedCaptureHeaders({ "x-trace-note": "ctx super-secret-value end" });
    expect(redacted?.["x-trace-note"]).not.toContain("super-secret-value");
  });

  it("redacts caller-declared sensitive header names regardless of case", () => {
    const redacted = redactedCaptureHeaders(
      {
        "X-Routing-Target": "staging-private-route",
        Accept: "text/plain",
      },
      ["x-routing-target"],
    );
    expect(redacted).toEqual({
      "X-Routing-Target": "[REDACTED]",
      Accept: "text/plain",
    });
  });

  it("flattens node's array-valued headers instead of dropping them", () => {
    // node:http exposes repeated headers as arrays; the standalone proxy feeds
    // those in directly.
    const redacted = redactedCaptureHeaders({
      "set-cookie": ["a=1", "b=2"],
      via: ["1.1 a", "1.1 b"],
    });
    expect(redacted?.["set-cookie"]).toBe("[REDACTED]");
    expect(redacted?.via).toBe("1.1 a, 1.1 b");
  });

  it.each([
    ["global", Headers],
    ["Undici", UndiciHeaders],
  ] as const)("accepts a %s Headers instance", (_name, HeadersConstructor) => {
    const redacted = redactedCaptureHeaders(
      new HeadersConstructor({ authorization: "Bearer x", accept: "text/plain" }),
    );
    expect(redacted?.authorization).toBe("[REDACTED]");
    expect(redacted?.accept).toBe("text/plain");
  });

  it("returns undefined when there are no headers", () => {
    expect(redactedCaptureHeaders(undefined)).toBeUndefined();
  });

  it("treats token-ish name fragments as sensitive", () => {
    const redacted = redactedCaptureHeaders({
      "x-vendor-access-token": "abc",
      "x-session-id": "s-1",
      "accept-language": "en-US",
    });
    expect(redacted).toEqual({
      "x-vendor-access-token": "[REDACTED]",
      "x-session-id": "[REDACTED]",
      "accept-language": "en-US",
    });
  });
});