diff --git a/server/src/__tests__/http-log-redaction.test.ts b/server/src/__tests__/http-log-redaction.test.ts index bee75c9128d..15a738ee89b 100644 --- a/server/src/__tests__/http-log-redaction.test.ts +++ b/server/src/__tests__/http-log-redaction.test.ts @@ -424,6 +424,7 @@ describe("HTTP logger redaction", () => { 'req.headers["x-telegram-bot-api-secret-token"]', ); expect(HTTP_LOG_REDACT_PATHS).toContain("reqBody.credentials"); + expect(HTTP_LOG_REDACT_PATHS).toContain("reqBody.credentialValues"); expect(HTTP_LOG_REDACT_PATHS).toContain("errorContext.details.credentials"); }); @@ -878,4 +879,37 @@ describe("HTTP logger redaction", () => { }); }, ); + + it("redacts a credentialValues envelope at the serialization boundary", async () => { + const chunks: string[] = []; + const stream = new Writable({ + write(chunk, _encoding, callback) { + chunks.push(chunk.toString()); + callback(); + }, + }); + const logger = pino({ redact: [...HTTP_LOG_REDACT_PATHS] }, stream); + + // No recursive redactor runs here on purpose: this exercises the path list + // as the backstop it is documented to be. fast-redact's censor is + // "[Redacted]" (pino's default), not redactSensitive's "[REDACTED]", so + // assert the secret is gone rather than pinning the marker. + logger.error( + { + errorContext: { name: "Error", message: "OAuth authorization expired." }, + reqBody: { + galleryKey: "github", + name: "GitHub", + credentialValues: { "credentials.authorization": "github_pat_canary" }, + }, + reqParams: {}, + }, + "POST /api/companies/company-1/tools/apps/connect 502 — OAuth authorization expired.", + ); + await new Promise((resolve) => setImmediate(resolve)); + + const output = chunks.join(""); + expect(output).not.toContain("github_pat_canary"); + expect(output).toContain('"galleryKey":"github"'); + }); }); diff --git a/server/src/__tests__/redact-sensitive.test.ts b/server/src/__tests__/redact-sensitive.test.ts index e3cb39280b5..633d4adb9bd 100644 --- a/server/src/__tests__/redact-sensitive.test.ts +++ b/server/src/__tests__/redact-sensitive.test.ts @@ -229,6 +229,33 @@ describe("redactSensitive", () => { }); describe("stripSecretBearingUrlParts", () => { + it("redacts the connector credentialValues envelope on an app-connect body", () => { + const out = redactSensitive({ + galleryKey: "github", + name: "GitHub", + credentialValues: { + "credentials.authorization": "authorization-canary", + "headers.X-Api-Key": "header-canary", + }, + }) as Record; + + // Diagnostics that make the failed connect debuggable must survive. + expect(out.galleryKey).toBe("github"); + expect(out.name).toBe("GitHub"); + expect(out.credentialValues).toBe("[REDACTED]"); + expect(JSON.stringify(out)).not.toContain("canary"); + }); + + it("redacts a credentialValues envelope nested below the top level", () => { + const out = redactSensitive({ + connection: { + credentialValues: { "credentials.authorization": "nested-canary" }, + }, + }); + + expect(JSON.stringify(out)).not.toContain("nested-canary"); + }); + it("keeps a request path legible while dropping its complete query and fragment", () => { expect( stripSecretBearingUrlParts( diff --git a/server/src/middleware/http-log-redaction.ts b/server/src/middleware/http-log-redaction.ts index 3817a40886e..88750f8d120 100644 --- a/server/src/middleware/http-log-redaction.ts +++ b/server/src/middleware/http-log-redaction.ts @@ -29,5 +29,11 @@ export const HTTP_LOG_REDACT_PATHS = [ // at the final serialization boundary in case a future custom serializer // bypasses the recursive redactor. "reqBody.credentials", + // The app-connect/reconnect body groups every provider credential under + // `credentialValues`: a map of provider config paths + // (`credentials.authorization`, `headers.X-Api-Key`) to the secret string for + // each. The key names are chosen by the caller, so no fixed list of leaf + // names can cover them. Redact the whole envelope instead. + "reqBody.credentialValues", "errorContext.details.credentials", ] as const; diff --git a/server/src/middleware/redact-sensitive.ts b/server/src/middleware/redact-sensitive.ts index 8817bac59b0..9401287ba27 100644 --- a/server/src/middleware/redact-sensitive.ts +++ b/server/src/middleware/redact-sensitive.ts @@ -27,6 +27,12 @@ const SENSITIVE_KEYS = new Set([ "configuration_token", "credential", "credentials", + // The app-connect/reconnect envelope. Its keys are caller-chosen provider + // config paths (`credentials.authorization`, `headers.X-Api-Key`), so they + // never match a leaf name in this set; redact the envelope by name. This is + // the durable half: the path list in http-log-redaction.ts only ever covers + // shapes someone has already seen leak. + "credentialvalues", "password", "currentpassword", "newpassword",