Redact bare Fireworks API keys (#98226)

* Redact bare Fireworks API keys

* fix(logging): harden Fireworks key redaction

* fix(logging): harden Fireworks key redaction

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
ooiuuii
2026-07-01 09:40:25 +08:00
committed by GitHub
parent 150ca2fedd
commit c5bc35de12
4 changed files with 63 additions and 7 deletions
+29 -2
View File
@@ -735,6 +735,11 @@ describe("redactSensitiveText", () => {
});
it("masks expanded vendor-prefix token corpus", () => {
const fireworksTokens = [
{ token: `fw-${"C".repeat(40)}`, redacted: "fw-CCC…CCCC" },
{ token: `fw_${"A".repeat(40)}`, redacted: "fw_AAA…AAAA" },
{ token: `fpk_${"B".repeat(40)}`, redacted: "fpk_BB…BBBB" },
];
const tokens = [
"sk-ant-abcdefghijklmnopqrstuvwxyz",
"gho_abcdefghijklmnopqrstuvwxyz",
@@ -797,6 +802,7 @@ describe("redactSensitiveText", () => {
"mem0_abcdefghijklmnopqrstuvwxyz",
"brv_abcdefghijklmnopqrstuvwxyz",
"xai-abcdefghijklmnopqrstuvwxyzABCDE",
...fireworksTokens.map(({ token }) => token),
];
// Redact each fixture alone so every vendor pattern proves it stays reachable through
// DEFAULT_REDACT_PREFILTER_RE; a joined corpus would let one trigger unlock all others.
@@ -817,15 +823,36 @@ describe("redactSensitiveText", () => {
expect(redactSensitiveText("xai-abcdefghijklmnopqrstuvwxyzABCDE", { mode: "tools" })).toBe(
"xai-ab…BCDE",
);
for (const { token, redacted } of fireworksTokens) {
expect(redactSensitiveText(token, { mode: "tools" })).toBe(redacted);
expect(redactSensitiveText(redacted, { mode: "tools" })).toBe(redacted);
}
});
it("does not redact ordinary identifiers containing short token-prefix substrings", () => {
const input =
"npm_telegram_package_spec ask_openclaw_query_patterns team_management risk_assessment glpat-docs dapi-example sbp_short nfp_site CCIPAT_docs ATATT-example";
const input = [
"npm_telegram_package_spec ask_openclaw_query_patterns team_management risk_assessment glpat-docs dapi-example sbp_short nfp_site CCIPAT_docs ATATT-example fw-tooshort fw_tooshort fpk_tooshort",
`fixturefw-${"C".repeat(40)}`,
`fixture_fw_${"A".repeat(40)}`,
`fixture_fpk_${"B".repeat(40)}`,
].join(" ");
const output = redactSensitiveText(input, { mode: "tools" });
expect(output).toBe(input);
});
it("masks Fireworks tokens that cross bounded-replacement chunk boundaries", () => {
const chunkSize = 16_384;
const prefix = `${"x".repeat(chunkSize - 2)} `;
const suffix = "y".repeat(chunkSize);
const tokens = [`fw-${"C".repeat(40)}`, `fw_${"A".repeat(40)}`, `fpk_${"B".repeat(40)}`];
for (const token of tokens) {
expect(redactSensitiveText(`${prefix}${token}${suffix}`, { mode: "tools" })).not.toContain(
token,
);
}
});
it("does not corrupt base64 blobs that embed token-prefix shapes", () => {
// Tiny-PNG base64 contains a gAAAA run from zero-filled IHDR bytes; pure-base64-alphabet
// prefixes must not fire mid-blob or media payloads get mangled.
+12 -5
View File
@@ -122,6 +122,7 @@ const STANDALONE_ASSIGNMENT_REDACT_PATTERN = String.raw`(^|[\s,;])(?:${STANDALON
// delimiters like `/` and `=` still qualify) but skip explicit `;base64,` payload spans, so
// data-URL media is never corrupted while tokens in URL paths or assignments still redact.
const BASE64_SAFE_TOKEN_BOUNDARY = String.raw`(^|[^A-Za-z0-9])(?<!;base64,[A-Za-z0-9+/=]*)`;
const IDENTIFIER_SAFE_TOKEN_BOUNDARY = String.raw`(^|[^A-Za-z0-9_])`;
const SHELL_REFERENCE_PRESERVING_PATTERN_SOURCES = new Set([
ENV_ASSIGNMENT_REDACT_PATTERN,
ESCAPED_ENV_ASSIGNMENT_REDACT_PATTERN,
@@ -129,9 +130,8 @@ const SHELL_REFERENCE_PRESERVING_PATTERN_SOURCES = new Set([
STANDALONE_ASSIGNMENT_REDACT_PATTERN,
]);
const shellReferencePreservingPatterns = new WeakSet<RegExp>();
// Patterns whose left-context assertions (BASE64_SAFE_TOKEN_BOUNDARY) break under chunked
// replacement: a chunk start satisfies `^` and hides the `;base64,` container from the
// lookbehind, so these must always run against the full string.
// Patterns whose left-context assertions or complete token can cross a chunk boundary must run
// against the full string; chunking can invent a `^` boundary or split the secret itself.
const chunkUnsafePatterns = new WeakSet<RegExp>();
const DEFAULT_REDACT_PATTERNS: string[] = [
@@ -235,6 +235,9 @@ const DEFAULT_REDACT_PATTERNS: string[] = [
String.raw`(mem0_[A-Za-z0-9]{10,})`,
String.raw`(brv_[A-Za-z0-9]{10,})`,
String.raw`(xai-[A-Za-z0-9]{30,})`,
String.raw`${IDENTIFIER_SAFE_TOKEN_BOUNDARY}(fw-[A-Za-z0-9]{30,})`,
String.raw`${IDENTIFIER_SAFE_TOKEN_BOUNDARY}(fw_[A-Za-z0-9]{30,})`,
String.raw`${IDENTIFIER_SAFE_TOKEN_BOUNDARY}(fpk_[A-Za-z0-9]{30,})`,
// Additional access-key and token-style prefixes.
String.raw`${BASE64_SAFE_TOKEN_BOUNDARY}(AKIA[A-Z0-9]{16})`,
String.raw`${BASE64_SAFE_TOKEN_BOUNDARY}(ASIA[A-Z0-9]{16})`,
@@ -261,7 +264,7 @@ const DEFAULT_REDACT_PREFILTER_SOURCES: string[] = [
// URL userinfo and connection-string password slots (`scheme://user:pass@host`).
String.raw`:\/\/[^\/\s:@]*:[^\/\s@]+@`,
// Vendor token prefixes and webhook hosts, ordered like DEFAULT_REDACT_PATTERNS.
String.raw`sk-|gh[opsur]_|github_pat_|glpat-|gloas-|xox[baprs]-|xapp-|hooks\.slack\.com|discord|gsk_|AIza|ya29\.|1\/\/0|eyJ|pplx-|fal_|fc-|bb_live_|gAAAA|[sr]k_(?:live|test)_|\bSG\.|npm_|pypi-|do[opr]_v1_|dp\.(?:ct|pt|sa|st|scim|audit)\.|dckr_|bkua_|CCIPAT_|sbp_|dapi[0-9a-f]|dd[pw]_|glsa_|nfp_|CFPAT-|ATCTT3|ATATT|ATBB|BBDC-|HRKU-|pat-(?:eu|na)1-|apify_api_|FlyV1|fio-u-|tvly-|exa_|syt_|retaindb_|mem0_|brv_|xai-`,
String.raw`sk-|gh[opsur]_|github_pat_|glpat-|gloas-|xox[baprs]-|xapp-|hooks\.slack\.com|discord|gsk_|AIza|ya29\.|1\/\/0|eyJ|pplx-|fal_|fc-|bb_live_|gAAAA|[sr]k_(?:live|test)_|\bSG\.|npm_|pypi-|do[opr]_v1_|dp\.(?:ct|pt|sa|st|scim|audit)\.|dckr_|bkua_|CCIPAT_|sbp_|dapi[0-9a-f]|dd[pw]_|glsa_|nfp_|CFPAT-|ATCTT3|ATATT|ATBB|BBDC-|HRKU-|pat-(?:eu|na)1-|apify_api_|FlyV1|fio-u-|tvly-|exa_|syt_|retaindb_|mem0_|brv_|xai-|fw-|fw_|fpk_`,
String.raw`(?:^|[^A-Za-z0-9_])(?:am_|sk_)`,
String.raw`A[KS]IA[A-Z0-9]|AKID|LTAI|hf_|api_org_|r8_`,
String.raw`\bbot\d{6,}:|\b\d{6,}:[A-Za-z0-9_-]{20,}`,
@@ -313,7 +316,11 @@ function parsePattern(raw: RedactPattern): RegExp | null {
if (pattern && typeof raw === "string" && SHELL_REFERENCE_PRESERVING_PATTERN_SOURCES.has(raw)) {
shellReferencePreservingPatterns.add(pattern);
}
if (pattern && typeof raw === "string" && raw.startsWith(BASE64_SAFE_TOKEN_BOUNDARY)) {
if (
pattern &&
typeof raw === "string" &&
(raw.startsWith(BASE64_SAFE_TOKEN_BOUNDARY) || raw.startsWith(IDENTIFIER_SAFE_TOKEN_BOUNDARY))
) {
chunkUnsafePatterns.add(pattern);
}
return pattern;
+19
View File
@@ -10,6 +10,9 @@ describe("browser tool detail redaction", () => {
"curl 'https://example.test?refresh_token=ya29.longOAuthRefreshTokenValue&ok=1'",
"client_secret=clientSecretValueThatShouldNotRender",
"AIzaSyDUMMYGoogleApiKeyValue1234567890",
`bare Fireworks key fw-${"C".repeat(40)}`,
`https://example.test?debug=fw_${"A".repeat(40)}&ok=1`,
`X-Debug: fpk_${"B".repeat(40)}`,
"-----BEGIN PRIVATE KEY-----\nabc123\n-----END PRIVATE KEY-----",
'cookie: "sessionid=verySensitiveCookieValue"',
].join("\n"),
@@ -27,8 +30,24 @@ describe("browser tool detail redaction", () => {
expect(redacted).not.toContain("longOAuthRefreshTokenValue");
expect(redacted).not.toContain("clientSecretValueThatShouldNotRender");
expect(redacted).not.toContain("DUMMYGoogleApiKeyValue1234567890");
expect(redacted).toContain("bare Fireworks key fw-CCC...CCCC");
expect(redacted).toContain("https://example.test?debug=fw_AAA...AAAA&ok=1");
expect(redacted).toContain("X-Debug: fpk_BB...BBBB");
expect(redacted).not.toContain("abc123");
expect(redacted).not.toContain("verySensitiveCookieValue");
for (const masked of ["fw-CCC...CCCC", "fw_AAA...AAAA", "fpk_BB...BBBB"]) {
expect(redactToolDetail(masked)).toBe(masked);
}
});
it("preserves long non-token identifiers containing Fireworks prefixes", () => {
const input = [
`fixturefw-${"C".repeat(40)}`,
`fixture_fw_${"A".repeat(40)}`,
`fixture_fpk_${"B".repeat(40)}`,
].join(" ");
expect(redactToolDetail(input)).toBe(input);
});
it("exposes the tool payload redaction name used by shared display modules", () => {
+3
View File
@@ -36,6 +36,9 @@ const SECRET_DETAIL_PATTERNS: RegExp[] = [
/\b(1\/\/0[0-9A-Za-z_\-./+=]{10,})\b/g,
/\b(eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g,
/\b(pplx-[A-Za-z0-9_-]{10,})\b/g,
/(^|[^A-Za-z0-9_])(fw-[A-Za-z0-9]{30,})/g,
/(^|[^A-Za-z0-9_])(fw_[A-Za-z0-9]{30,})/g,
/(^|[^A-Za-z0-9_])(fpk_[A-Za-z0-9]{30,})/g,
/\b(npm_[A-Za-z0-9]{10,})\b/g,
/\b(AKID[A-Za-z0-9]{10,})\b/g,
/\b(LTAI[A-Za-z0-9]{10,})\b/g,