Skip to content

Commit b1ceac3

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(security): redact API header table diagnostics
1 parent 568a539 commit b1ceac3

3 files changed

Lines changed: 115 additions & 0 deletions

File tree

apps/sim/executor/execution/block-executor.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,53 @@ describe('BlockExecutor', () => {
9494
mockUploadFile.mockImplementation(async ({ customKey }) => ({ key: customKey }))
9595
})
9696

97+
it.each([false, true])(
98+
'redacts header diagnostics while preserving execution values (stringified: %s)',
99+
async (stringified) => {
100+
const headers = [
101+
{ cells: { Key: 'aUtHoRiZaTiOn', Value: 'synthetic-header-value' } },
102+
{ cells: { Key: 'Accept', Value: 'application/json' } },
103+
]
104+
const block = createBlock()
105+
block.metadata = { id: BlockType.API, name: 'API' }
106+
block.config.params = { headers: stringified ? JSON.stringify(headers) : headers }
107+
const originalBlock = structuredClone(block)
108+
const workflow: SerializedWorkflow = {
109+
version: '1',
110+
blocks: [block],
111+
connections: [],
112+
loops: {},
113+
parallels: {},
114+
}
115+
const state = new ExecutionState()
116+
const resolver = new VariableResolver(workflow, {}, state)
117+
const onBlockComplete = vi.fn(async () => {})
118+
const handler: BlockHandler = {
119+
canHandle: () => true,
120+
execute: async (_ctx, _block, inputs) => {
121+
expect(inputs.headers).toEqual(block.config.params.headers)
122+
return { headers }
123+
},
124+
}
125+
const executor = new BlockExecutor([handler], resolver, { onBlockComplete }, state)
126+
const ctx = createContext(state)
127+
128+
await executor.execute(ctx, createNode(block), block)
129+
await vi.waitFor(() => expect(onBlockComplete).toHaveBeenCalledOnce())
130+
131+
const displayInput = {
132+
headers: [
133+
{ cells: { Key: 'aUtHoRiZaTiOn', Value: '[REDACTED]' } },
134+
{ cells: { Key: 'Accept', Value: 'application/json' } },
135+
],
136+
}
137+
expect(ctx.blockLogs[0]?.input).toEqual(displayInput)
138+
expect(onBlockComplete.mock.calls[0]?.[3]?.input).toEqual(displayInput)
139+
expect(state.getBlockOutput(block.id)).toEqual({ headers })
140+
expect(block).toEqual(originalBlock)
141+
}
142+
)
143+
97144
it('persists function output arrays as manifests in execution state', async () => {
98145
const block = createBlock()
99146
const workflow: SerializedWorkflow = {

apps/sim/lib/core/security/redaction.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,50 @@ describe('redactSensitiveValues', () => {
488488
})
489489

490490
describe('redactApiKeys', () => {
491+
describe('HTTP headers', () => {
492+
it.each(['aUtHoRiZaTiOn', 'Proxy-Authorization', 'X-Api-Key', 'Cookie', 'Set-Cookie'])(
493+
'redacts %s in header maps and table rows without mutating the input',
494+
(name) => {
495+
const row = { id: 'header-row', cells: { Key: name, Value: 'synthetic-header-value' } }
496+
const input = {
497+
request: { headers: [row, { cells: { Key: 'Accept', Value: 'application/json' } }] },
498+
responses: [
499+
{ headers: { [name]: 'synthetic-header-value', 'Content-Type': 'text/plain' } },
500+
],
501+
}
502+
const original = structuredClone(input)
503+
504+
const result = redactApiKeys(input)
505+
506+
expect(result.request.headers).toEqual([
507+
{ id: 'header-row', cells: { Key: name, Value: '[REDACTED]' } },
508+
{ cells: { Key: 'Accept', Value: 'application/json' } },
509+
])
510+
expect(result.responses[0].headers).toEqual({
511+
[name]: '[REDACTED]',
512+
'Content-Type': 'text/plain',
513+
})
514+
expect(input).toEqual(original)
515+
expect(redactApiKeys(result)).toEqual(result)
516+
}
517+
)
518+
519+
it('does not treat unrelated tables or cookie fields as HTTP headers', () => {
520+
const input = {
521+
rows: [{ cells: { Key: 'Authorization', Value: 'ordinary-table-value' } }],
522+
Cookie: 'ordinary-field-value',
523+
headers: [
524+
null,
525+
{},
526+
{ cells: { Key: 'Authorization' } },
527+
{ cells: { Key: 1, Value: 'ok' } },
528+
],
529+
}
530+
531+
expect(redactApiKeys(input)).toEqual(input)
532+
})
533+
})
534+
491535
describe('object redaction', () => {
492536
it.concurrent('should redact sensitive keys in flat objects', () => {
493537
const obj = {

apps/sim/lib/core/security/redaction.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,28 @@ export function isLargeDataKey(key: string): boolean {
449449
return LARGE_DATA_KEYS.has(key)
450450
}
451451

452+
/** Redacts supported HTTP header representations without changing the source values. */
453+
function redactHeaders(headers: unknown): unknown {
454+
const redacted = redactApiKeys(headers)
455+
const isSensitiveHeader = (name: string) =>
456+
isSensitiveKey(name) || /^(?:set-)?cookie$/i.test(name)
457+
458+
if (Array.isArray(redacted)) {
459+
for (const row of redacted) {
460+
const cells = row?.cells
461+
if (cells && typeof cells.Key === 'string' && isSensitiveHeader(cells.Key)) {
462+
if (Object.hasOwn(cells, 'Value')) cells.Value = REDACTED_MARKER
463+
}
464+
}
465+
} else if (redacted && typeof redacted === 'object') {
466+
for (const name of Object.keys(redacted)) {
467+
if (isSensitiveHeader(name)) redacted[name] = REDACTED_MARKER
468+
}
469+
}
470+
471+
return redacted
472+
}
473+
452474
export function redactApiKeys(obj: any): any {
453475
if (obj === null || obj === undefined) {
454476
return obj
@@ -482,6 +504,8 @@ export function redactApiKeys(obj: any): any {
482504
result[key] = REDACTED_MARKER
483505
} else if (isLargeDataKey(key) && typeof value === 'string') {
484506
result[key] = TRUNCATED_MARKER
507+
} else if (key.toLowerCase() === 'headers') {
508+
result[key] = redactHeaders(value)
485509
} else if (typeof value === 'object' && value !== null) {
486510
result[key] = redactApiKeys(value)
487511
} else {

0 commit comments

Comments
 (0)