diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e81ec31108..ddeb7b95251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai ### Changes - Docker: run the runtime image under `tini` so long-lived containers reap orphaned child processes and forward signals correctly. (#77885) Thanks @VintageAyu. +- Logging/redaction: redact quoted HTTP client secret fields and auth/cookie headers in shared log and formatted error output. Related #71211 and #65623. (#75033) Thanks @liaoandi. - Google/Gemini: normalize retired `google/gemini-3-pro-preview` and `google-gemini-cli/gemini-3-pro-preview` selections to `google/gemini-3.1-pro-preview` before they are written to model config. - Amazon Bedrock: support `serviceTier` parameter for Bedrock models, configurable via `agents.defaults.params.serviceTier` or per-model in `agents.defaults.models`. Valid values: `default`, `flex`, `priority`, `reserved`. (#64512) Thanks @mobilinkd. - Control UI: read the Quick Settings exec policy badge from `tools.exec.security` instead of the non-schema `agents.defaults.exec.security` path, so configured `full`/`deny` values render accurately. Fixes #78311. Thanks @FriedBack. diff --git a/src/infra/errors.test.ts b/src/infra/errors.test.ts index 6b9d67cca38..bc3703a2a44 100644 --- a/src/infra/errors.test.ts +++ b/src/infra/errors.test.ts @@ -95,6 +95,25 @@ describe("error helpers", () => { expect(formatted).not.toContain(token); }); + it("redacts HTTP client config secrets from formatted error chains", () => { + const appSecret = "feishu_app_secret_1234567890"; + const tenantToken = "feishu_tenant_access_abcdef123456"; + const rootCause = new Error( + `request config: { appSecret: '${appSecret}', headers: { authorization: 'Bearer ${tenantToken}' } }`, + ); + const httpError = Object.assign(new Error(`POST /auth/v3/tenant_access_token failed`), { + cause: rootCause, + }); + + const formatted = formatErrorMessage(httpError); + + expect(formatted).toContain("POST /auth/v3/tenant_access_token failed"); + expect(formatted).toContain("appSecret:"); + expect(formatted).toContain("authorization:"); + expect(formatted).not.toContain(appSecret); + expect(formatted).not.toContain(tenantToken); + }); + it.each([ { value: new Error("Unhandled stop reason: refusal_policy"), diff --git a/src/logging/redact.test.ts b/src/logging/redact.test.ts index 22907235443..9432313b5a5 100644 --- a/src/logging/redact.test.ts +++ b/src/logging/redact.test.ts @@ -110,6 +110,24 @@ describe("redactSensitiveText", () => { ); }); + it("masks HTTP client config secrets in JSON and object-inspection fields", () => { + const appSecret = "feishu_app_secret_1234567890"; + const clientSecret = "oauth_client_secret_1234567890"; + const input = [ + `body: {"app_secret":"${appSecret}"}`, + `config: { appSecret: '${appSecret}', client_secret: '${clientSecret}' }`, + ].join("\n"); + const output = redactSensitiveText(input, { + mode: "tools", + patterns: defaults, + }); + expect(output).toContain('"app_secret":"feishu…7890"'); + expect(output).toContain("appSecret: 'feishu…7890'"); + expect(output).toContain("client_secret: 'oauth_…7890'"); + expect(output).not.toContain(appSecret); + expect(output).not.toContain(clientSecret); + }); + it("masks payment credential assignments and flags", () => { const input = [ "LINK_CARD_NUMBER=4242424242424242", @@ -133,6 +151,20 @@ describe("redactSensitiveText", () => { expect(output).toContain("--card-number ***"); }); + it("masks quoted HTTP auth headers in object-inspection fields", () => { + const bearer = "feishu_tenant_access_abcdef123456"; + const cookie = "session_cookie_value_abcdef123456"; + const input = `headers: { authorization: 'Bearer ${bearer}', cookie: '${cookie}' }`; + const output = redactSensitiveText(input, { + mode: "tools", + patterns: defaults, + }); + expect(output).toContain("authorization: 'Bearer…3456'"); + expect(output).toContain("cookie: 'sessio…3456'"); + expect(output).not.toContain(bearer); + expect(output).not.toContain(cookie); + }); + it("masks payment credential URL query parameters", () => { const input = "POST /authorize?shared_payment_token=spt_abcdefghijklmnopqrstuvwxyz&card_number=4242424242424242&amount=4200"; diff --git a/src/logging/redact.ts b/src/logging/redact.ts index 50476b77958..6f6e2aa37c0 100644 --- a/src/logging/redact.ts +++ b/src/logging/redact.ts @@ -14,7 +14,7 @@ const PAYMENT_CREDENTIAL_ENV_KEYS = String.raw`CARD[_-]?NUMBER|CARD[_-]?CVC|CARD const PAYMENT_CREDENTIAL_QUERY_KEYS = String.raw`card[-_]?number|card[-_]?cvc|card[-_]?cvv|cvc|cvv|security[-_]?code|payment[-_]?credential|shared[-_]?payment[-_]?token`; const PAYMENT_CREDENTIAL_JSON_KEYS = String.raw`cardNumber|card_number|cardCvc|card_cvc|cardCvv|card_cvv|cvc|cvv|securityCode|security_code|paymentCredential|payment_credential|sharedPaymentToken|shared_payment_token`; const STRUCTURED_SECRET_FIELD_RE = new RegExp( - String.raw`^(?:api[-_]?key|apiKey|token|secret|password|passwd|access[-_]?token|accessToken|refresh[-_]?token|refreshToken|client[-_]?secret|clientSecret|${PAYMENT_CREDENTIAL_QUERY_KEYS}|${PAYMENT_CREDENTIAL_JSON_KEYS})$`, + String.raw`^(?:api[-_]?key|apiKey|token|secret|password|passwd|access[-_]?token|accessToken|refresh[-_]?token|refreshToken|auth[-_]?token|authToken|client[-_]?secret|clientSecret|app[-_]?secret|appSecret|${PAYMENT_CREDENTIAL_QUERY_KEYS}|${PAYMENT_CREDENTIAL_JSON_KEYS})$`, "i", ); @@ -27,6 +27,10 @@ const DEFAULT_REDACT_PATTERNS: string[] = [ String.raw`/[?&](?:access[-_]?token|auth[-_]?token|hook[-_]?token|refresh[-_]?token|api[-_]?key|client[-_]?secret|token|key|secret|password|pass|passwd|auth|signature|${PAYMENT_CREDENTIAL_QUERY_KEYS})=([^&\s"'<>]+)/gi`, // JSON fields. String.raw`"(?:apiKey|token|secret|password|passwd|accessToken|refreshToken|${PAYMENT_CREDENTIAL_JSON_KEYS})"\s*:\s*"([^"]+)"`, + // HTTP client diagnostics often stringify request config objects using + // JSON or util.inspect-style fields rather than env/CLI syntax. + String.raw`(^|[\s,{])["']?(?:api[-_]key|access[-_]token|refresh[-_]token|authToken|auth[-_]token|clientSecret|client[-_]secret|appSecret|app[-_]secret)["']?\s*[:=]\s*(["'])([^"'\r\n]+)\2`, + String.raw`(^|[\s,{])["']?(?:authorization|proxy-authorization|cookie|set-cookie|x-api-key|x-auth-token)["']?\s*[:=]\s*(["'])([^"'\r\n]+)\2`, // CLI flags. String.raw`--(?:api[-_]?key|hook[-_]?token|token|secret|password|passwd|${PAYMENT_CREDENTIAL_QUERY_KEYS})\s+(["']?)([^\s"']+)\1`, // Authorization headers. @@ -34,7 +38,7 @@ const DEFAULT_REDACT_PATTERNS: string[] = [ String.raw`\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b`, // Standalone token assignments in CLI or HTTP diagnostics. URL query params // are handled above so non-secret params survive and long values stay hinted. - String.raw`(^|[\s,;])(?:access_token|refresh_token|api[-_]?key|token|secret|password|passwd|${PAYMENT_CREDENTIAL_QUERY_KEYS})=([^\s&#]+)`, + String.raw`(^|[\s,;])(?:access_token|refresh_token|auth[-_]?token|api[-_]?key|client[-_]?secret|app[-_]?secret|token|secret|password|passwd|${PAYMENT_CREDENTIAL_QUERY_KEYS})=([^\s&#]+)`, // PEM blocks. String.raw`-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----`, // Common token prefixes.