Files
sim/apps/sim/lib/core/security/redaction.test.ts
Vikhyath Mondreti 1f1f015031 improvement(files): update execution for passing base64 strings (#2906)
* progress

* improvement(execution): update execution for passing base64 strings

* fix types

* cleanup comments

* path security vuln

* reject promise correctly

* fix redirect case

* remove proxy routes

* fix tests

* use ipaddr
2026-01-20 17:49:00 -08:00

772 lines
24 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
isLargeDataKey,
isSensitiveKey,
REDACTED_MARKER,
redactApiKeys,
redactSensitiveValues,
sanitizeEventData,
sanitizeForLogging,
TRUNCATED_MARKER,
} from './redaction'
/**
* Security-focused edge case tests for redaction utilities
*/
describe('REDACTED_MARKER', () => {
it.concurrent('should be the standard marker', () => {
expect(REDACTED_MARKER).toBe('[REDACTED]')
})
})
describe('TRUNCATED_MARKER', () => {
it.concurrent('should be the standard marker', () => {
expect(TRUNCATED_MARKER).toBe('[TRUNCATED]')
})
})
describe('isLargeDataKey', () => {
it.concurrent('should identify base64 as large data key', () => {
expect(isLargeDataKey('base64')).toBe(true)
})
it.concurrent('should not identify other keys as large data', () => {
expect(isLargeDataKey('content')).toBe(false)
expect(isLargeDataKey('data')).toBe(false)
expect(isLargeDataKey('base')).toBe(false)
})
})
describe('isSensitiveKey', () => {
describe('exact matches', () => {
it.concurrent('should match apiKey variations', () => {
expect(isSensitiveKey('apiKey')).toBe(true)
expect(isSensitiveKey('api_key')).toBe(true)
expect(isSensitiveKey('api-key')).toBe(true)
expect(isSensitiveKey('APIKEY')).toBe(true)
expect(isSensitiveKey('API_KEY')).toBe(true)
})
it.concurrent('should match token variations', () => {
expect(isSensitiveKey('access_token')).toBe(true)
expect(isSensitiveKey('refresh_token')).toBe(true)
expect(isSensitiveKey('auth_token')).toBe(true)
expect(isSensitiveKey('accessToken')).toBe(true)
})
it.concurrent('should match secret variations', () => {
expect(isSensitiveKey('client_secret')).toBe(true)
expect(isSensitiveKey('clientSecret')).toBe(true)
expect(isSensitiveKey('secret')).toBe(true)
})
it.concurrent('should match other sensitive keys', () => {
expect(isSensitiveKey('private_key')).toBe(true)
expect(isSensitiveKey('authorization')).toBe(true)
expect(isSensitiveKey('bearer')).toBe(true)
expect(isSensitiveKey('private')).toBe(true)
expect(isSensitiveKey('auth')).toBe(true)
expect(isSensitiveKey('password')).toBe(true)
expect(isSensitiveKey('credential')).toBe(true)
})
})
describe('suffix matches', () => {
it.concurrent('should match keys ending in secret', () => {
expect(isSensitiveKey('clientSecret')).toBe(true)
expect(isSensitiveKey('appSecret')).toBe(true)
expect(isSensitiveKey('mySecret')).toBe(true)
})
it.concurrent('should match keys ending in password', () => {
expect(isSensitiveKey('userPassword')).toBe(true)
expect(isSensitiveKey('dbPassword')).toBe(true)
expect(isSensitiveKey('adminPassword')).toBe(true)
})
it.concurrent('should match keys ending in token', () => {
expect(isSensitiveKey('accessToken')).toBe(true)
expect(isSensitiveKey('refreshToken')).toBe(true)
expect(isSensitiveKey('bearerToken')).toBe(true)
})
it.concurrent('should match keys ending in credential', () => {
expect(isSensitiveKey('userCredential')).toBe(true)
expect(isSensitiveKey('dbCredential')).toBe(true)
})
})
describe('non-sensitive keys (no false positives)', () => {
it.concurrent('should not match keys with sensitive words as prefix only', () => {
expect(isSensitiveKey('tokenCount')).toBe(false)
expect(isSensitiveKey('tokenizer')).toBe(false)
expect(isSensitiveKey('secretKey')).toBe(false)
expect(isSensitiveKey('passwordStrength')).toBe(false)
expect(isSensitiveKey('authMethod')).toBe(false)
})
it.concurrent('should match keys ending with sensitive words (intentional)', () => {
expect(isSensitiveKey('hasSecret')).toBe(true)
expect(isSensitiveKey('userPassword')).toBe(true)
expect(isSensitiveKey('sessionToken')).toBe(true)
})
it.concurrent('should not match normal field names', () => {
expect(isSensitiveKey('name')).toBe(false)
expect(isSensitiveKey('email')).toBe(false)
expect(isSensitiveKey('id')).toBe(false)
expect(isSensitiveKey('value')).toBe(false)
expect(isSensitiveKey('data')).toBe(false)
expect(isSensitiveKey('count')).toBe(false)
expect(isSensitiveKey('status')).toBe(false)
})
})
})
describe('redactSensitiveValues', () => {
it.concurrent('should redact Bearer tokens', () => {
const input = 'Authorization: Bearer abc123xyz456'
const result = redactSensitiveValues(input)
expect(result).toBe('Authorization: Bearer [REDACTED]')
expect(result).not.toContain('abc123xyz456')
})
it.concurrent('should redact Basic auth', () => {
const input = 'Authorization: Basic dXNlcjpwYXNz'
const result = redactSensitiveValues(input)
expect(result).toBe('Authorization: Basic [REDACTED]')
})
it.concurrent('should redact API key prefixes', () => {
const input = 'Using key sk-1234567890abcdefghijklmnop'
const result = redactSensitiveValues(input)
expect(result).toContain('[REDACTED]')
expect(result).not.toContain('sk-1234567890abcdefghijklmnop')
})
it.concurrent('should redact JSON-style password fields', () => {
const input = 'password: "mysecretpass123"'
const result = redactSensitiveValues(input)
expect(result).toContain('[REDACTED]')
expect(result).not.toContain('mysecretpass123')
})
it.concurrent('should redact JSON-style token fields', () => {
const input = 'token: "tokenvalue123"'
const result = redactSensitiveValues(input)
expect(result).toContain('[REDACTED]')
expect(result).not.toContain('tokenvalue123')
})
it.concurrent('should redact JSON-style api_key fields', () => {
const input = 'api_key: "key123456"'
const result = redactSensitiveValues(input)
expect(result).toContain('[REDACTED]')
expect(result).not.toContain('key123456')
})
it.concurrent('should not modify safe strings', () => {
const input = 'This is a normal string with no secrets'
const result = redactSensitiveValues(input)
expect(result).toBe(input)
})
it.concurrent('should handle empty strings', () => {
expect(redactSensitiveValues('')).toBe('')
})
it.concurrent('should handle null/undefined gracefully', () => {
expect(redactSensitiveValues(null as any)).toBe(null)
expect(redactSensitiveValues(undefined as any)).toBe(undefined)
})
})
describe('redactApiKeys', () => {
describe('object redaction', () => {
it.concurrent('should redact sensitive keys in flat objects', () => {
const obj = {
apiKey: 'secret-key',
api_key: 'another-secret',
access_token: 'token-value',
secret: 'secret-value',
password: 'password-value',
normalField: 'normal-value',
}
const result = redactApiKeys(obj)
expect(result.apiKey).toBe('[REDACTED]')
expect(result.api_key).toBe('[REDACTED]')
expect(result.access_token).toBe('[REDACTED]')
expect(result.secret).toBe('[REDACTED]')
expect(result.password).toBe('[REDACTED]')
expect(result.normalField).toBe('normal-value')
})
it.concurrent('should redact sensitive keys in nested objects', () => {
const obj = {
config: {
apiKey: 'secret-key',
normalField: 'normal-value',
},
}
const result = redactApiKeys(obj)
expect(result.config.apiKey).toBe('[REDACTED]')
expect(result.config.normalField).toBe('normal-value')
})
it.concurrent('should redact sensitive keys in arrays', () => {
const arr = [{ apiKey: 'secret-key-1' }, { apiKey: 'secret-key-2' }]
const result = redactApiKeys(arr)
expect(result[0].apiKey).toBe('[REDACTED]')
expect(result[1].apiKey).toBe('[REDACTED]')
})
it.concurrent('should handle deeply nested structures', () => {
const obj = {
users: [
{
name: 'John',
credentials: {
apiKey: 'secret-key',
username: 'john_doe',
},
},
],
config: {
database: {
password: 'db-password',
host: 'localhost',
},
},
}
const result = redactApiKeys(obj)
expect(result.users[0].name).toBe('John')
expect(result.users[0].credentials.apiKey).toBe('[REDACTED]')
expect(result.users[0].credentials.username).toBe('john_doe')
expect(result.config.database.password).toBe('[REDACTED]')
expect(result.config.database.host).toBe('localhost')
})
it.concurrent('should truncate base64 fields', () => {
const obj = {
id: 'file-123',
name: 'document.pdf',
base64: 'VGhpcyBpcyBhIHZlcnkgbG9uZyBiYXNlNjQgc3RyaW5n...',
size: 12345,
}
const result = redactApiKeys(obj)
expect(result.id).toBe('file-123')
expect(result.name).toBe('document.pdf')
expect(result.base64).toBe('[TRUNCATED]')
expect(result.size).toBe(12345)
})
it.concurrent('should truncate base64 in nested UserFile objects', () => {
const obj = {
files: [
{
id: 'file-1',
name: 'doc1.pdf',
url: 'http://example.com/file1',
size: 1000,
base64: 'base64content1...',
},
{
id: 'file-2',
name: 'doc2.pdf',
url: 'http://example.com/file2',
size: 2000,
base64: 'base64content2...',
},
],
}
const result = redactApiKeys(obj)
expect(result.files[0].id).toBe('file-1')
expect(result.files[0].base64).toBe('[TRUNCATED]')
expect(result.files[1].base64).toBe('[TRUNCATED]')
})
it.concurrent('should filter UserFile objects to only expose allowed fields', () => {
const obj = {
processedFiles: [
{
id: 'file-123',
name: 'document.pdf',
url: 'http://localhost/api/files/serve/...',
size: 12345,
type: 'application/pdf',
key: 'execution/workspace/workflow/file.pdf',
context: 'execution',
base64: 'VGhpcyBpcyBhIGJhc2U2NCBzdHJpbmc=',
},
],
}
const result = redactApiKeys(obj)
// Exposed fields should be present
expect(result.processedFiles[0].id).toBe('file-123')
expect(result.processedFiles[0].name).toBe('document.pdf')
expect(result.processedFiles[0].url).toBe('http://localhost/api/files/serve/...')
expect(result.processedFiles[0].size).toBe(12345)
expect(result.processedFiles[0].type).toBe('application/pdf')
expect(result.processedFiles[0].base64).toBe('[TRUNCATED]')
// Internal fields should be filtered out
expect(result.processedFiles[0]).not.toHaveProperty('key')
expect(result.processedFiles[0]).not.toHaveProperty('context')
})
})
describe('primitive handling', () => {
it.concurrent('should return primitives unchanged', () => {
expect(redactApiKeys('string')).toBe('string')
expect(redactApiKeys(123)).toBe(123)
expect(redactApiKeys(true)).toBe(true)
expect(redactApiKeys(null)).toBe(null)
expect(redactApiKeys(undefined)).toBe(undefined)
})
})
describe('no false positives', () => {
it.concurrent('should not redact keys with sensitive words as prefix only', () => {
const obj = {
tokenCount: 100,
secretKey: 'not-actually-secret',
passwordStrength: 'strong',
authMethod: 'oauth',
}
const result = redactApiKeys(obj)
expect(result.tokenCount).toBe(100)
expect(result.secretKey).toBe('not-actually-secret')
expect(result.passwordStrength).toBe('strong')
expect(result.authMethod).toBe('oauth')
})
})
})
describe('sanitizeForLogging', () => {
it.concurrent('should truncate long strings', () => {
const longString = 'a'.repeat(200)
const result = sanitizeForLogging(longString, 50)
expect(result.length).toBe(50)
})
it.concurrent('should use default max length of 100', () => {
const longString = 'a'.repeat(200)
const result = sanitizeForLogging(longString)
expect(result.length).toBe(100)
})
it.concurrent('should redact sensitive patterns', () => {
const input = 'Bearer abc123xyz456'
const result = sanitizeForLogging(input)
expect(result).toContain('[REDACTED]')
expect(result).not.toContain('abc123xyz456')
})
it.concurrent('should handle empty strings', () => {
expect(sanitizeForLogging('')).toBe('')
})
it.concurrent('should not modify safe short strings', () => {
const input = 'Safe string'
const result = sanitizeForLogging(input)
expect(result).toBe(input)
})
})
describe('sanitizeEventData', () => {
describe('object sanitization', () => {
it.concurrent('should remove sensitive keys entirely', () => {
const event = {
action: 'login',
apiKey: 'secret-key',
password: 'secret-pass',
userId: '123',
}
const result = sanitizeEventData(event)
expect(result.action).toBe('login')
expect(result.userId).toBe('123')
expect(result).not.toHaveProperty('apiKey')
expect(result).not.toHaveProperty('password')
})
it.concurrent('should redact sensitive patterns in string values', () => {
const event = {
message: 'Auth: Bearer abc123token',
normal: 'normal value',
}
const result = sanitizeEventData(event)
expect(result.message).toContain('[REDACTED]')
expect(result.message).not.toContain('abc123token')
expect(result.normal).toBe('normal value')
})
it.concurrent('should handle nested objects', () => {
const event = {
user: {
id: '123',
accessToken: 'secret-token',
},
}
const result = sanitizeEventData(event)
expect(result.user.id).toBe('123')
expect(result.user).not.toHaveProperty('accessToken')
})
it.concurrent('should handle arrays', () => {
const event = {
items: [
{ id: 1, apiKey: 'key1' },
{ id: 2, apiKey: 'key2' },
],
}
const result = sanitizeEventData(event)
expect(result.items[0].id).toBe(1)
expect(result.items[0]).not.toHaveProperty('apiKey')
expect(result.items[1].id).toBe(2)
expect(result.items[1]).not.toHaveProperty('apiKey')
})
})
describe('primitive handling', () => {
it.concurrent('should return primitives appropriately', () => {
expect(sanitizeEventData(null)).toBe(null)
expect(sanitizeEventData(undefined)).toBe(undefined)
expect(sanitizeEventData(123)).toBe(123)
expect(sanitizeEventData(true)).toBe(true)
})
it.concurrent('should redact sensitive patterns in top-level strings', () => {
const result = sanitizeEventData('Bearer secrettoken123')
expect(result).toContain('[REDACTED]')
})
it.concurrent('should not redact normal strings', () => {
const result = sanitizeEventData('normal string')
expect(result).toBe('normal string')
})
})
describe('no false positives', () => {
it.concurrent('should not remove keys with sensitive words in middle', () => {
const event = {
tokenCount: 500,
isAuthenticated: true,
hasSecretFeature: false,
}
const result = sanitizeEventData(event)
expect(result.tokenCount).toBe(500)
expect(result.isAuthenticated).toBe(true)
expect(result.hasSecretFeature).toBe(false)
})
})
})
describe('Security edge cases', () => {
describe('redactApiKeys security', () => {
it.concurrent('should handle objects with prototype-like key names safely', () => {
const obj = {
protoField: { isAdmin: true },
name: 'test',
apiKey: 'secret',
}
const result = redactApiKeys(obj)
expect(result.name).toBe('test')
expect(result.protoField).toEqual({ isAdmin: true })
expect(result.apiKey).toBe('[REDACTED]')
})
it.concurrent('should handle objects with constructor key', () => {
const obj = {
constructor: 'test-value',
normalField: 'normal',
}
const result = redactApiKeys(obj)
expect(result.constructor).toBe('test-value')
expect(result.normalField).toBe('normal')
})
it.concurrent('should handle objects with toString key', () => {
const obj = {
toString: 'custom-tostring',
valueOf: 'custom-valueof',
apiKey: 'secret',
}
const result = redactApiKeys(obj)
expect(result.toString).toBe('custom-tostring')
expect(result.valueOf).toBe('custom-valueof')
expect(result.apiKey).toBe('[REDACTED]')
})
it.concurrent('should not mutate original object', () => {
const original = {
apiKey: 'secret-key',
nested: {
password: 'secret-password',
},
}
const originalCopy = JSON.parse(JSON.stringify(original))
redactApiKeys(original)
expect(original).toEqual(originalCopy)
})
it.concurrent('should handle very deeply nested structures', () => {
let obj: any = { data: 'value' }
for (let i = 0; i < 50; i++) {
obj = { nested: obj, apiKey: `secret-${i}` }
}
const result = redactApiKeys(obj)
expect(result.apiKey).toBe('[REDACTED]')
expect(result.nested.apiKey).toBe('[REDACTED]')
})
it.concurrent('should handle arrays with mixed types', () => {
const arr = [
{ apiKey: 'secret' },
'string',
123,
null,
undefined,
true,
[{ password: 'nested' }],
]
const result = redactApiKeys(arr)
expect(result[0].apiKey).toBe('[REDACTED]')
expect(result[1]).toBe('string')
expect(result[2]).toBe(123)
expect(result[3]).toBe(null)
expect(result[4]).toBe(undefined)
expect(result[5]).toBe(true)
expect(result[6][0].password).toBe('[REDACTED]')
})
it.concurrent('should handle empty arrays', () => {
const result = redactApiKeys([])
expect(result).toEqual([])
})
it.concurrent('should handle empty objects', () => {
const result = redactApiKeys({})
expect(result).toEqual({})
})
})
describe('redactSensitiveValues security', () => {
it.concurrent('should handle multiple API key patterns in one string', () => {
const input = 'Keys: sk-abc123defghijklmnopqr and pk-xyz789abcdefghijklmnop'
const result = redactSensitiveValues(input)
expect(result).not.toContain('sk-abc123defghijklmnopqr')
expect(result).not.toContain('pk-xyz789abcdefghijklmnop')
expect(result.match(/\[REDACTED\]/g)?.length).toBeGreaterThanOrEqual(2)
})
it.concurrent('should handle multiline strings with sensitive data', () => {
const input = `Line 1: Bearer token123abc456def
Line 2: password: "secretpass"
Line 3: Normal content`
const result = redactSensitiveValues(input)
expect(result).toContain('[REDACTED]')
expect(result).not.toContain('token123abc456def')
expect(result).not.toContain('secretpass')
expect(result).toContain('Normal content')
})
it.concurrent('should handle unicode in strings', () => {
const input = 'Bearer abc123'
const result = redactSensitiveValues(input)
expect(result).toContain('[REDACTED]')
expect(result).not.toContain('abc123')
})
it.concurrent('should handle very long strings', () => {
const longSecret = 'a'.repeat(10000)
const input = `Bearer ${longSecret}`
const result = redactSensitiveValues(input)
expect(result).toContain('[REDACTED]')
expect(result.length).toBeLessThan(input.length)
})
it.concurrent('should not match partial patterns', () => {
const input = 'This is a Bear without er suffix'
const result = redactSensitiveValues(input)
expect(result).toBe(input)
})
it.concurrent('should handle special regex characters safely', () => {
const input = 'Test with special chars: $^.*+?()[]{}|'
const result = redactSensitiveValues(input)
expect(result).toBe(input)
})
})
describe('sanitizeEventData security', () => {
it.concurrent('should strip sensitive keys entirely (not redact)', () => {
const event = {
action: 'login',
apiKey: 'should-be-stripped',
password: 'should-be-stripped',
userId: '123',
}
const result = sanitizeEventData(event)
expect(result).not.toHaveProperty('apiKey')
expect(result).not.toHaveProperty('password')
expect(Object.keys(result)).not.toContain('apiKey')
expect(Object.keys(result)).not.toContain('password')
})
it.concurrent('should handle Symbol keys gracefully', () => {
const sym = Symbol('test')
const event: any = {
[sym]: 'symbol-value',
normalKey: 'normal-value',
}
expect(() => sanitizeEventData(event)).not.toThrow()
})
it.concurrent('should handle Date objects as objects', () => {
const date = new Date('2024-01-01')
const event = {
createdAt: date,
apiKey: 'secret',
}
const result = sanitizeEventData(event)
expect(result.createdAt).toBeDefined()
expect(result).not.toHaveProperty('apiKey')
})
it.concurrent('should handle objects with numeric keys', () => {
const event: any = {
0: 'first',
1: 'second',
apiKey: 'secret',
}
const result = sanitizeEventData(event)
expect(result[0]).toBe('first')
expect(result[1]).toBe('second')
expect(result).not.toHaveProperty('apiKey')
})
})
describe('isSensitiveKey security', () => {
it.concurrent('should handle case variations', () => {
expect(isSensitiveKey('APIKEY')).toBe(true)
expect(isSensitiveKey('ApiKey')).toBe(true)
expect(isSensitiveKey('apikey')).toBe(true)
expect(isSensitiveKey('API_KEY')).toBe(true)
expect(isSensitiveKey('api_key')).toBe(true)
expect(isSensitiveKey('Api_Key')).toBe(true)
})
it.concurrent('should handle empty string', () => {
expect(isSensitiveKey('')).toBe(false)
})
it.concurrent('should handle very long key names', () => {
const longKey = `${'a'.repeat(10000)}password`
expect(isSensitiveKey(longKey)).toBe(true)
})
it.concurrent('should handle keys with special characters', () => {
expect(isSensitiveKey('api-key')).toBe(true)
expect(isSensitiveKey('api_key')).toBe(true)
})
it.concurrent('should detect oauth tokens', () => {
expect(isSensitiveKey('access_token')).toBe(true)
expect(isSensitiveKey('refresh_token')).toBe(true)
expect(isSensitiveKey('accessToken')).toBe(true)
expect(isSensitiveKey('refreshToken')).toBe(true)
})
it.concurrent('should detect various credential patterns', () => {
expect(isSensitiveKey('userCredential')).toBe(true)
expect(isSensitiveKey('dbCredential')).toBe(true)
expect(isSensitiveKey('appCredential')).toBe(true)
})
})
describe('sanitizeForLogging edge cases', () => {
it.concurrent('should handle string with only sensitive content', () => {
const input = 'Bearer abc123xyz456'
const result = sanitizeForLogging(input)
expect(result).toContain('[REDACTED]')
expect(result).not.toContain('abc123xyz456')
})
it.concurrent('should truncate strings to specified length', () => {
const longString = 'a'.repeat(200)
const result = sanitizeForLogging(longString, 60)
expect(result.length).toBe(60)
})
it.concurrent('should handle maxLength of 0', () => {
const result = sanitizeForLogging('test', 0)
expect(result).toBe('')
})
it.concurrent('should handle negative maxLength gracefully', () => {
const result = sanitizeForLogging('test', -5)
expect(result).toBe('')
})
it.concurrent('should handle maxLength larger than string', () => {
const input = 'short'
const result = sanitizeForLogging(input, 1000)
expect(result).toBe(input)
})
})
})