diff --git a/apps/sim/executor/execution/block-executor.test.ts b/apps/sim/executor/execution/block-executor.test.ts index 250542ee7be..6e862dc2a29 100644 --- a/apps/sim/executor/execution/block-executor.test.ts +++ b/apps/sim/executor/execution/block-executor.test.ts @@ -94,6 +94,53 @@ describe('BlockExecutor', () => { mockUploadFile.mockImplementation(async ({ customKey }) => ({ key: customKey })) }) + it.each([false, true])( + 'redacts header diagnostics while preserving execution values (stringified: %s)', + async (stringified) => { + const headers = [ + { cells: { Key: 'aUtHoRiZaTiOn', Value: 'synthetic-header-value' } }, + { cells: { Key: 'Accept', Value: 'application/json' } }, + ] + const block = createBlock() + block.metadata = { id: BlockType.API, name: 'API' } + block.config.params = { headers: stringified ? JSON.stringify(headers) : headers } + const originalBlock = structuredClone(block) + const workflow: SerializedWorkflow = { + version: '1', + blocks: [block], + connections: [], + loops: {}, + parallels: {}, + } + const state = new ExecutionState() + const resolver = new VariableResolver(workflow, {}, state) + const onBlockComplete = vi.fn(async () => {}) + const handler: BlockHandler = { + canHandle: () => true, + execute: async (_ctx, _block, inputs) => { + expect(inputs.headers).toEqual(block.config.params.headers) + return { headers } + }, + } + const executor = new BlockExecutor([handler], resolver, { onBlockComplete }, state) + const ctx = createContext(state) + + await executor.execute(ctx, createNode(block), block) + await vi.waitFor(() => expect(onBlockComplete).toHaveBeenCalledOnce()) + + const displayInput = { + headers: [ + { cells: { Key: 'aUtHoRiZaTiOn', Value: '[REDACTED]' } }, + { cells: { Key: 'Accept', Value: 'application/json' } }, + ], + } + expect(ctx.blockLogs[0]?.input).toEqual(displayInput) + expect(onBlockComplete.mock.calls[0]?.[3]?.input).toEqual(displayInput) + expect(state.getBlockOutput(block.id)).toEqual({ headers }) + expect(block).toEqual(originalBlock) + } + ) + it('persists function output arrays as manifests in execution state', async () => { const block = createBlock() const workflow: SerializedWorkflow = { diff --git a/apps/sim/lib/core/security/redaction.test.ts b/apps/sim/lib/core/security/redaction.test.ts index ac493b709ad..9b65cc66404 100644 --- a/apps/sim/lib/core/security/redaction.test.ts +++ b/apps/sim/lib/core/security/redaction.test.ts @@ -488,6 +488,50 @@ describe('redactSensitiveValues', () => { }) describe('redactApiKeys', () => { + describe('HTTP headers', () => { + it.each(['aUtHoRiZaTiOn', 'Proxy-Authorization', 'X-Api-Key', 'Cookie', 'Set-Cookie'])( + 'redacts %s in header maps and table rows without mutating the input', + (name) => { + const row = { id: 'header-row', cells: { Key: name, Value: 'synthetic-header-value' } } + const input = { + request: { headers: [row, { cells: { Key: 'Accept', Value: 'application/json' } }] }, + responses: [ + { headers: { [name]: 'synthetic-header-value', 'Content-Type': 'text/plain' } }, + ], + } + const original = structuredClone(input) + + const result = redactApiKeys(input) + + expect(result.request.headers).toEqual([ + { id: 'header-row', cells: { Key: name, Value: '[REDACTED]' } }, + { cells: { Key: 'Accept', Value: 'application/json' } }, + ]) + expect(result.responses[0].headers).toEqual({ + [name]: '[REDACTED]', + 'Content-Type': 'text/plain', + }) + expect(input).toEqual(original) + expect(redactApiKeys(result)).toEqual(result) + } + ) + + it('does not treat unrelated tables or cookie fields as HTTP headers', () => { + const input = { + rows: [{ cells: { Key: 'Authorization', Value: 'ordinary-table-value' } }], + Cookie: 'ordinary-field-value', + headers: [ + null, + {}, + { cells: { Key: 'Authorization' } }, + { cells: { Key: 1, Value: 'ok' } }, + ], + } + + expect(redactApiKeys(input)).toEqual(input) + }) + }) + describe('object redaction', () => { it.concurrent('should redact sensitive keys in flat objects', () => { const obj = { diff --git a/apps/sim/lib/core/security/redaction.ts b/apps/sim/lib/core/security/redaction.ts index af6ea2c5487..1e685470081 100644 --- a/apps/sim/lib/core/security/redaction.ts +++ b/apps/sim/lib/core/security/redaction.ts @@ -449,6 +449,28 @@ export function isLargeDataKey(key: string): boolean { return LARGE_DATA_KEYS.has(key) } +/** Redacts supported HTTP header representations without changing the source values. */ +function redactHeaders(headers: unknown): unknown { + const redacted = redactApiKeys(headers) + const isSensitiveHeader = (name: string) => + isSensitiveKey(name) || /^(?:set-)?cookie$/i.test(name) + + if (Array.isArray(redacted)) { + for (const row of redacted) { + const cells = row?.cells + if (cells && typeof cells.Key === 'string' && isSensitiveHeader(cells.Key)) { + if (Object.hasOwn(cells, 'Value')) cells.Value = REDACTED_MARKER + } + } + } else if (redacted && typeof redacted === 'object') { + for (const name of Object.keys(redacted)) { + if (isSensitiveHeader(name)) redacted[name] = REDACTED_MARKER + } + } + + return redacted +} + export function redactApiKeys(obj: any): any { if (obj === null || obj === undefined) { return obj @@ -482,6 +504,8 @@ export function redactApiKeys(obj: any): any { result[key] = REDACTED_MARKER } else if (isLargeDataKey(key) && typeof value === 'string') { result[key] = TRUNCATED_MARKER + } else if (key.toLowerCase() === 'headers') { + result[key] = redactHeaders(value) } else if (typeof value === 'object' && value !== null) { result[key] = redactApiKeys(value) } else {