From 6c1399df73a404b26b18c831d90dcd0862ac025d Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Wed, 8 Jul 2026 16:01:06 -0400 Subject: [PATCH 1/7] fix(web-api): redact response body in debug logs Apply redact() to API response bodies before debug logging, consistent with how request bodies and headers are already handled. This prevents tokens returned in API responses (e.g., oauth.v2.access, openid.connect.token) from being written to logs in plaintext when debug logging is enabled. Fixes #2651 --- packages/web-api/src/WebClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web-api/src/WebClient.ts b/packages/web-api/src/WebClient.ts index 3a5185a33..e3a2998ff 100644 --- a/packages/web-api/src/WebClient.ts +++ b/packages/web-api/src/WebClient.ts @@ -371,7 +371,7 @@ export class WebClient extends Methods { headers, ); const result = await this.buildResult(response); - this.logger.debug(`http request result: ${JSON.stringify(result)}`); + this.logger.debug(`http request result: ${JSON.stringify(redact(result))}`); // log warnings in response metadata if (result.response_metadata !== undefined && result.response_metadata.warnings !== undefined) { @@ -1072,7 +1072,7 @@ export function buildThreadTsWarningMessage(method: string): string { * @param body * @returns */ -function redact(body: Record): Record { +function redact(body: object): Record { // biome-ignore lint/suspicious/noExplicitAny: objects can be anything const flattened = Object.entries(body).map<[string, any] | []>(([key, value]) => { // no value provided From 7db839183abe5e710579c6fe3c52d96f867b50a7 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 9 Jul 2026 10:36:34 -0400 Subject: [PATCH 2/7] fix(web-api): add test for response body redaction in debug logs Adds a unit test verifying that tokens in API response bodies are redacted before being written to debug logs. Fixes #2651 --- packages/web-api/src/WebClient.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/web-api/src/WebClient.test.ts b/packages/web-api/src/WebClient.test.ts index 4dcd4c851..c2fb61fda 100644 --- a/packages/web-api/src/WebClient.test.ts +++ b/packages/web-api/src/WebClient.test.ts @@ -118,6 +118,21 @@ describe('WebClient', () => { // Calling #setName of the given logger is destructive assert.strictEqual((logger.setName as sinon.SinonStub).called, false); }); + + it('should redact tokens in API response bodies in debug logs', async () => { + const scope = nock('https://slack.com') + .post('/api/oauth.v2.access') + .reply(200, { ok: true, access_token: 'xoxb-secret-token', token_type: 'bot' }); + const client = new WebClient(undefined, { logLevel: LogLevel.DEBUG, logger }); + await client.apiCall('oauth.v2.access', { code: 'test-code' }); + scope.done(); + + const debugCalls = (logger.debug as sinon.SinonStub).getCalls(); + const resultLog = debugCalls.find((call) => call.args[0].includes('http request result')); + assert.ok(resultLog, 'expected a debug log for the http request result'); + assert.ok(resultLog.args[0].includes('[[REDACTED]]'), 'expected access_token to be redacted'); + assert.ok(!resultLog.args[0].includes('xoxb-secret-token'), 'expected raw token to not appear in logs'); + }); }); describe('has an option to override the Axios timeout value', () => { From 3631334ff7c8a3c80e2936463b7e18f3b8a1781b Mon Sep 17 00:00:00 2001 From: Ale Mercado <104795114+srtaalej@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:56:19 -0400 Subject: [PATCH 3/7] Update packages/web-api/src/WebClient.test.ts Co-authored-by: William Bergamin --- packages/web-api/src/WebClient.test.ts | 27 +++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/web-api/src/WebClient.test.ts b/packages/web-api/src/WebClient.test.ts index c2fb61fda..0cf036e79 100644 --- a/packages/web-api/src/WebClient.test.ts +++ b/packages/web-api/src/WebClient.test.ts @@ -122,7 +122,32 @@ describe('WebClient', () => { it('should redact tokens in API response bodies in debug logs', async () => { const scope = nock('https://slack.com') .post('/api/oauth.v2.access') - .reply(200, { ok: true, access_token: 'xoxb-secret-token', token_type: 'bot' }); + .reply(200, { + "ok": true, + "access_token": "xoxb-secret-token", + "token_type": "bot", + "scope": "commands,incoming-webhook", + "bot_user_id": "U0KRQLJ9H", + "app_id": "A0KRD7HC3", + "expires_in": 43200, + "refresh_token": "xoxe-secret-token", + "team": { + "name": "Slack Softball Team", + "id": "T9TK3CUKW" + }, + "enterprise": { + "name": "slack-sports", + "id": "E12345678" + }, + "authed_user": { + "id": "U1234", + "scope": "chat:write", + "access_token": "xoxe.xoxp-secret-token", + "expires_in": 43200, + "refresh_token": "xoxe-secret-token", + "token_type": "user" + } + }); const client = new WebClient(undefined, { logLevel: LogLevel.DEBUG, logger }); await client.apiCall('oauth.v2.access', { code: 'test-code' }); scope.done(); From cc327fd388eb8345110d012c9047fece13c8507b Mon Sep 17 00:00:00 2001 From: Ale Mercado <104795114+srtaalej@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:56:27 -0400 Subject: [PATCH 4/7] Update packages/web-api/src/WebClient.test.ts Co-authored-by: William Bergamin --- packages/web-api/src/WebClient.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/web-api/src/WebClient.test.ts b/packages/web-api/src/WebClient.test.ts index 0cf036e79..f4ba3b7d4 100644 --- a/packages/web-api/src/WebClient.test.ts +++ b/packages/web-api/src/WebClient.test.ts @@ -156,7 +156,10 @@ describe('WebClient', () => { const resultLog = debugCalls.find((call) => call.args[0].includes('http request result')); assert.ok(resultLog, 'expected a debug log for the http request result'); assert.ok(resultLog.args[0].includes('[[REDACTED]]'), 'expected access_token to be redacted'); - assert.ok(!resultLog.args[0].includes('xoxb-secret-token'), 'expected raw token to not appear in logs'); + assert.ok(!resultLog.args[0].includes('xoxb-secret-token'), 'expected raw bot token to not appear in logs'); + assert.ok(!resultLog.args[0].includes('xoxe-secret-token'), 'expected raw refresh token to not appear in logs'); + assert.ok(!resultLog.args[0].includes('xoxe.xoxp-secret-token'), 'expected raw user token to not appear in logs'); + assert.ok(!resultLog.args[0].includes('xoxe-secret-token'), 'expected raw user token to not appear in logs'); }); }); From e4a2198ac6ff71e2a2e776bbe6ae4c651d0ac01d Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 9 Jul 2026 10:58:29 -0400 Subject: [PATCH 5/7] fix(web-api): make redact() recurse into nested objects The redact() function previously only operated on top-level keys, meaning nested tokens (e.g., authed_user.access_token in oauth.v2.access responses) would still appear in debug logs. Now recurses into nested objects to redact token keys at any depth. --- packages/web-api/src/WebClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web-api/src/WebClient.ts b/packages/web-api/src/WebClient.ts index e3a2998ff..e2ab7329b 100644 --- a/packages/web-api/src/WebClient.ts +++ b/packages/web-api/src/WebClient.ts @@ -1090,8 +1090,8 @@ function redact(body: object): Record { // when value is buffer or stream we can avoid logging it if (Buffer.isBuffer(value) || isStream(value)) { serializedValue = '[[BINARY VALUE OMITTED]]'; - } else if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') { - serializedValue = JSON.stringify(value); + } else if (typeof value === 'object') { + serializedValue = redact(value); } return [key, serializedValue]; }); From e3f5cfe5c78af9fac49e69bdc70917bf91ad211a Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 9 Jul 2026 10:59:52 -0400 Subject: [PATCH 6/7] chore: add changeset for response body redaction fix --- .changeset/redact-response-debug-logs.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/redact-response-debug-logs.md diff --git a/.changeset/redact-response-debug-logs.md b/.changeset/redact-response-debug-logs.md new file mode 100644 index 000000000..71c52e0c3 --- /dev/null +++ b/.changeset/redact-response-debug-logs.md @@ -0,0 +1,5 @@ +--- +"@slack/web-api": patch +--- + +fix: apply redact() to API response bodies in debug logs and recurse into nested objects, preventing tokens from leaking into logs when debug logging is enabled From 4fc31ac3acbef5ae7a41a89950e2a1dca5dcd36a Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 9 Jul 2026 12:22:33 -0400 Subject: [PATCH 7/7] style: fix formatting in redaction test --- packages/web-api/src/WebClient.test.ts | 52 +++++++++++++------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/web-api/src/WebClient.test.ts b/packages/web-api/src/WebClient.test.ts index f4ba3b7d4..d599cebc7 100644 --- a/packages/web-api/src/WebClient.test.ts +++ b/packages/web-api/src/WebClient.test.ts @@ -123,31 +123,31 @@ describe('WebClient', () => { const scope = nock('https://slack.com') .post('/api/oauth.v2.access') .reply(200, { - "ok": true, - "access_token": "xoxb-secret-token", - "token_type": "bot", - "scope": "commands,incoming-webhook", - "bot_user_id": "U0KRQLJ9H", - "app_id": "A0KRD7HC3", - "expires_in": 43200, - "refresh_token": "xoxe-secret-token", - "team": { - "name": "Slack Softball Team", - "id": "T9TK3CUKW" - }, - "enterprise": { - "name": "slack-sports", - "id": "E12345678" - }, - "authed_user": { - "id": "U1234", - "scope": "chat:write", - "access_token": "xoxe.xoxp-secret-token", - "expires_in": 43200, - "refresh_token": "xoxe-secret-token", - "token_type": "user" - } - }); + ok: true, + access_token: 'xoxb-secret-token', + token_type: 'bot', + scope: 'commands,incoming-webhook', + bot_user_id: 'U0KRQLJ9H', + app_id: 'A0KRD7HC3', + expires_in: 43200, + refresh_token: 'xoxe-secret-token', + team: { + name: 'Slack Softball Team', + id: 'T9TK3CUKW', + }, + enterprise: { + name: 'slack-sports', + id: 'E12345678', + }, + authed_user: { + id: 'U1234', + scope: 'chat:write', + access_token: 'xoxe.xoxp-secret-token', + expires_in: 43200, + refresh_token: 'xoxe-secret-token', + token_type: 'user', + }, + }); const client = new WebClient(undefined, { logLevel: LogLevel.DEBUG, logger }); await client.apiCall('oauth.v2.access', { code: 'test-code' }); scope.done(); @@ -158,7 +158,7 @@ describe('WebClient', () => { assert.ok(resultLog.args[0].includes('[[REDACTED]]'), 'expected access_token to be redacted'); assert.ok(!resultLog.args[0].includes('xoxb-secret-token'), 'expected raw bot token to not appear in logs'); assert.ok(!resultLog.args[0].includes('xoxe-secret-token'), 'expected raw refresh token to not appear in logs'); - assert.ok(!resultLog.args[0].includes('xoxe.xoxp-secret-token'), 'expected raw user token to not appear in logs'); + assert.ok(!resultLog.args[0].includes('xoxe.xoxp-secret-token'), 'expected raw user token to not appear in logs'); assert.ok(!resultLog.args[0].includes('xoxe-secret-token'), 'expected raw user token to not appear in logs'); }); });