Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion apps/extension/src/debug/__tests__/redact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,41 @@

import { describe, expect, it } from "vitest";
import { jsonPointer, parseJsonSource } from "../json-source";
import { BODY_CHARS, redactBody, redactRequestUrl } from "../redact";
import { BODY_CHARS, redactBody, redactHeaders, redactRequestUrl } from "../redact";

describe("source-preserving evidence redaction", () => {
it.each([
"PRIVATE-TOKEN",
"x-goog-api-key",
"x-amz-security-token",
"x-access-token",
"X-Vault-Token",
"Ocp-Apim-Subscription-Key",
"x-client-secret",
])("redacts the vendor credential header %s", (name) => {
expect(redactHeaders({ [name]: "BSK_SYNTHETIC_SECRET_7264" })).toEqual({
[name.toLowerCase()]: "[redacted]",
});
});

it("redacts vendor credential fields in JSON bodies", () => {
const text =
'{"private_token":"BSK_SYNTHETIC_SECRET_7264","x_goog_api_key":"BSK_SYNTHETIC_SECRET_7264","page":2}';
expect(redactBody(text, "application/json")).toMatchObject({
text: '{"private_token":"[redacted]","x_goog_api_key":"[redacted]","page":2}',
redacted: true,
});
});

it.each([
"content-type",
"x-request-id",
"idempotency-key",
"x-tokenizer",
])("keeps the ordinary header %s", (name) => {
expect(redactHeaders({ [name]: "value-7264" })).toEqual({ [name]: "value-7264" });
});

it.each([
'type="password"',
'type="password"',
Expand Down
18 changes: 16 additions & 2 deletions apps/extension/src/debug/redact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,25 @@ const MASK = "[redacted]";
export const BODY_CHARS = 64 * 1024;
export const URL_CHARS = 2048;

/**
* Vendor credential names that name their secret in their last part: GitLab `private-token` (and the
* `private_token` query parameter), Google `x-goog-api-key`, AWS `x-amz-security-token`, Azure
* `ocp-apim-subscription-key`, Vault `x-vault-token`.
*/
const SECRET_HEADER_SUFFIX =
/(?:^|[_-])(?:token|api[_-]?key|apikey|secret|subscription[_-]key|password|passwd)$/i;

function secretHeader(name: string): boolean {
return SECRET.test(name) || SECRET_HEADER_SUFFIX.test(name);
}

function secretField(key: string): boolean {
return key
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.split(/[.[\]]+/)
.some((part) => SECRET.test(part) || /(?:^|[_-])(?:password|passwd|pwd)(?:[_-]|$)/i.test(part));
.some(
(part) => secretHeader(part) || /(?:^|[_-])(?:password|passwd|pwd)(?:[_-]|$)/i.test(part),
);
}

const HTML_FIELD_SEPARATORS: Record<string, string> = {
Expand Down Expand Up @@ -97,7 +111,7 @@ export function redactHeaderEvidence(value: unknown): {
let remaining = 4 * 1024;
for (const [key, raw] of entries.slice(0, 80)) {
const name = key.slice(0, 128).toLowerCase();
const secret = SECRET.test(name);
const secret = secretHeader(name);
const original = String(raw);
const redacted = secret ? MASK : redactText(original, 2048);
const limit = Math.max(0, Math.min(2048, remaining - name.length));
Expand Down
Loading