Skip to content

Commit 47ef0b2

Browse files
committed
fix(redaction): consolidate redaction utils, apply them to inputs and outputs before persisting logs
1 parent 65787d7 commit 47ef0b2

File tree

8 files changed

+233
-87
lines changed

8 files changed

+233
-87
lines changed

apps/sim/app/api/auth/sso/register/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server'
22
import { z } from 'zod'
33
import { auth } from '@/lib/auth'
44
import { env } from '@/lib/core/config/env'
5+
import { REDACTED_MARKER } from '@/lib/core/security/redaction'
56
import { createLogger } from '@/lib/logs/console/logger'
67

78
const logger = createLogger('SSO-Register')
@@ -236,13 +237,13 @@ export async function POST(request: NextRequest) {
236237
oidcConfig: providerConfig.oidcConfig
237238
? {
238239
...providerConfig.oidcConfig,
239-
clientSecret: '[REDACTED]',
240+
clientSecret: REDACTED_MARKER,
240241
}
241242
: undefined,
242243
samlConfig: providerConfig.samlConfig
243244
? {
244245
...providerConfig.samlConfig,
245-
cert: '[REDACTED]',
246+
cert: REDACTED_MARKER,
246247
}
247248
: undefined,
248249
},

apps/sim/app/api/tools/stagehand/agent/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type NextRequest, NextResponse } from 'next/server'
22
import { z } from 'zod'
33
import { env } from '@/lib/core/config/env'
4+
import { isSensitiveKey, REDACTED_MARKER } from '@/lib/core/security/redaction'
45
import { createLogger } from '@/lib/logs/console/logger'
56
import { ensureZodObject, normalizeUrl } from '@/app/api/tools/stagehand/utils'
67

@@ -188,7 +189,7 @@ export async function POST(request: NextRequest) {
188189

189190
if (variablesObject && Object.keys(variablesObject).length > 0) {
190191
const safeVarKeys = Object.keys(variablesObject).map((key) => {
191-
return key.toLowerCase().includes('password') ? `${key}: [REDACTED]` : key
192+
return isSensitiveKey(key) ? `${key}: ${REDACTED_MARKER}` : key
192193
})
193194
logger.info('Variables available for task', { variables: safeVarKeys })
194195
}

apps/sim/instrumentation-client.ts

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import { env } from './lib/core/config/env'
6+
import { sanitizeEventData } from './lib/core/security/redaction'
67

78
if (typeof window !== 'undefined') {
89
const TELEMETRY_STATUS_KEY = 'simstudio-telemetry-status'
@@ -41,37 +42,6 @@ if (typeof window !== 'undefined') {
4142
}
4243
}
4344

44-
/**
45-
* Sanitize event data to remove sensitive information
46-
*/
47-
function sanitizeEvent(event: any): any {
48-
const patterns = ['password', 'token', 'secret', 'key', 'auth', 'credential', 'private']
49-
const sensitiveRe = new RegExp(patterns.join('|'), 'i')
50-
51-
const scrubString = (s: string) => (s && sensitiveRe.test(s) ? '[redacted]' : s)
52-
53-
if (event == null) return event
54-
if (typeof event === 'string') return scrubString(event)
55-
if (typeof event !== 'object') return event
56-
57-
if (Array.isArray(event)) {
58-
return event.map((item) => sanitizeEvent(item))
59-
}
60-
61-
const sanitized: Record<string, unknown> = {}
62-
for (const [key, value] of Object.entries(event)) {
63-
const lowerKey = key.toLowerCase()
64-
if (patterns.some((p) => lowerKey.includes(p))) continue
65-
66-
if (typeof value === 'string') sanitized[key] = scrubString(value)
67-
else if (Array.isArray(value)) sanitized[key] = value.map((v) => sanitizeEvent(v))
68-
else if (value && typeof value === 'object') sanitized[key] = sanitizeEvent(value)
69-
else sanitized[key] = value
70-
}
71-
72-
return sanitized
73-
}
74-
7545
/**
7646
* Flush batch of events to server
7747
*/
@@ -84,7 +54,7 @@ if (typeof window !== 'undefined') {
8454
batchTimer = null
8555
}
8656

87-
const sanitizedBatch = batch.map(sanitizeEvent)
57+
const sanitizedBatch = batch.map(sanitizeEventData)
8858

8959
const payload = JSON.stringify({
9060
category: 'batch',

apps/sim/lib/core/security/input-validation.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { describe, expect, it } from 'vitest'
22
import {
33
createPinnedUrl,
4-
sanitizeForLogging,
54
validateAlphanumericId,
65
validateEnum,
76
validateFileExtension,
@@ -11,6 +10,7 @@ import {
1110
validateUrlWithDNS,
1211
validateUUID,
1312
} from '@/lib/core/security/input-validation'
13+
import { sanitizeForLogging } from '@/lib/core/security/redaction'
1414

1515
describe('validatePathSegment', () => {
1616
describe('valid inputs', () => {

apps/sim/lib/core/security/input-validation.ts

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -556,29 +556,6 @@ export function validateFileExtension(
556556
return { isValid: true, sanitized: normalizedExt }
557557
}
558558

559-
/**
560-
* Sanitizes a string for safe logging (removes potential sensitive data patterns)
561-
*
562-
* @param value - The value to sanitize
563-
* @param maxLength - Maximum length to return (default: 100)
564-
* @returns Sanitized string safe for logging
565-
*/
566-
export function sanitizeForLogging(value: string, maxLength = 100): string {
567-
if (!value) return ''
568-
569-
// Truncate long values
570-
let sanitized = value.substring(0, maxLength)
571-
572-
// Mask common sensitive patterns
573-
sanitized = sanitized
574-
.replace(/Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, 'Bearer [REDACTED]')
575-
.replace(/password['":\s]*['"]\w+['"]/gi, 'password: "[REDACTED]"')
576-
.replace(/token['":\s]*['"]\w+['"]/gi, 'token: "[REDACTED]"')
577-
.replace(/api[_-]?key['":\s]*['"]\w+['"]/gi, 'api_key: "[REDACTED]"')
578-
579-
return sanitized
580-
}
581-
582559
/**
583560
* Validates Microsoft Graph API resource IDs
584561
*
Lines changed: 172 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,122 @@
11
/**
2-
* Recursively redacts API keys in an object
3-
* @param obj The object to redact API keys from
4-
* @returns A new object with API keys redacted
2+
* Centralized redaction utilities for sensitive data
53
*/
6-
export const redactApiKeys = (obj: any): any => {
7-
if (!obj || typeof obj !== 'object') {
4+
5+
/** Standard marker used for all redacted values */
6+
export const REDACTED_MARKER = '[REDACTED]'
7+
8+
/**
9+
* Patterns for sensitive key names (case-insensitive matching)
10+
* These patterns match common naming conventions for sensitive data
11+
*/
12+
const SENSITIVE_KEY_PATTERNS: RegExp[] = [
13+
/^api[_-]?key$/i,
14+
/^access[_-]?token$/i,
15+
/^refresh[_-]?token$/i,
16+
/^client[_-]?secret$/i,
17+
/^private[_-]?key$/i,
18+
/^auth[_-]?token$/i,
19+
/\bsecret\b/i,
20+
/\bpassword\b/i,
21+
/\btoken\b/i,
22+
/\bcredential\b/i,
23+
/\bauthorization\b/i,
24+
/\bbearer\b/i,
25+
/\bprivate\b/i,
26+
/\bauth\b/i,
27+
]
28+
29+
/**
30+
* Patterns for sensitive values in strings (for redacting values, not keys)
31+
* Each pattern has a replacement function
32+
*/
33+
const SENSITIVE_VALUE_PATTERNS: Array<{
34+
pattern: RegExp
35+
replacement: string
36+
}> = [
37+
// Bearer tokens
38+
{
39+
pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi,
40+
replacement: `Bearer ${REDACTED_MARKER}`,
41+
},
42+
// Basic auth
43+
{
44+
pattern: /Basic\s+[A-Za-z0-9+/]+=*/gi,
45+
replacement: `Basic ${REDACTED_MARKER}`,
46+
},
47+
// API keys that look like sk-..., pk-..., etc.
48+
{
49+
pattern: /\b(sk|pk|api|key)[_-][A-Za-z0-9\-._]{20,}\b/gi,
50+
replacement: REDACTED_MARKER,
51+
},
52+
// JSON-style password fields: password: "value" or password: 'value'
53+
{
54+
pattern: /password['":\s]*['"][^'"]+['"]/gi,
55+
replacement: `password: "${REDACTED_MARKER}"`,
56+
},
57+
// JSON-style token fields: token: "value" or token: 'value'
58+
{
59+
pattern: /token['":\s]*['"][^'"]+['"]/gi,
60+
replacement: `token: "${REDACTED_MARKER}"`,
61+
},
62+
// JSON-style api_key fields: api_key: "value" or api-key: "value"
63+
{
64+
pattern: /api[_-]?key['":\s]*['"][^'"]+['"]/gi,
65+
replacement: `api_key: "${REDACTED_MARKER}"`,
66+
},
67+
]
68+
69+
/**
70+
* Checks if a key name matches any sensitive pattern
71+
* @param key - The key name to check
72+
* @returns True if the key is considered sensitive
73+
*/
74+
export function isSensitiveKey(key: string): boolean {
75+
const lowerKey = key.toLowerCase()
76+
return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(lowerKey))
77+
}
78+
79+
/**
80+
* Redacts sensitive patterns from a string value
81+
* @param value - The string to redact
82+
* @returns The string with sensitive patterns redacted
83+
*/
84+
export function redactSensitiveValues(value: string): string {
85+
if (!value || typeof value !== 'string') {
86+
return value
87+
}
88+
89+
let result = value
90+
for (const { pattern, replacement } of SENSITIVE_VALUE_PATTERNS) {
91+
result = result.replace(pattern, replacement)
92+
}
93+
return result
94+
}
95+
96+
/**
97+
* Recursively redacts sensitive data (API keys, passwords, tokens, etc.) from an object
98+
*
99+
* @param obj - The object to redact sensitive data from
100+
* @returns A new object with sensitive data redacted
101+
*/
102+
export function redactApiKeys(obj: any): any {
103+
if (obj === null || obj === undefined) {
104+
return obj
105+
}
106+
107+
if (typeof obj !== 'object') {
8108
return obj
9109
}
10110

11111
if (Array.isArray(obj)) {
12-
return obj.map(redactApiKeys)
112+
return obj.map((item) => redactApiKeys(item))
13113
}
14114

15115
const result: Record<string, any> = {}
16116

17117
for (const [key, value] of Object.entries(obj)) {
18-
if (
19-
key.toLowerCase() === 'apikey' ||
20-
key.toLowerCase() === 'api_key' ||
21-
key.toLowerCase() === 'access_token' ||
22-
/\bsecret\b/i.test(key.toLowerCase()) ||
23-
/\bpassword\b/i.test(key.toLowerCase())
24-
) {
25-
result[key] = '***REDACTED***'
118+
if (isSensitiveKey(key)) {
119+
result[key] = REDACTED_MARKER
26120
} else if (typeof value === 'object' && value !== null) {
27121
result[key] = redactApiKeys(value)
28122
} else {
@@ -32,3 +126,67 @@ export const redactApiKeys = (obj: any): any => {
32126

33127
return result
34128
}
129+
130+
/**
131+
* Sanitizes a string for safe logging by truncating and redacting sensitive patterns
132+
*
133+
* @param value - The string to sanitize
134+
* @param maxLength - Maximum length of the output (default: 100)
135+
* @returns The sanitized string
136+
*/
137+
export function sanitizeForLogging(value: string, maxLength = 100): string {
138+
if (!value) return ''
139+
140+
let sanitized = value.substring(0, maxLength)
141+
142+
sanitized = redactSensitiveValues(sanitized)
143+
144+
return sanitized
145+
}
146+
147+
/**
148+
* Sanitizes event data for error reporting/analytics
149+
*
150+
* @param event - The event data to sanitize
151+
* @returns Sanitized event data safe for external reporting
152+
*/
153+
export function sanitizeEventData(event: any): any {
154+
if (event === null || event === undefined) {
155+
return event
156+
}
157+
158+
if (typeof event === 'string') {
159+
if (isSensitiveKey(event)) {
160+
return REDACTED_MARKER
161+
}
162+
return redactSensitiveValues(event)
163+
}
164+
165+
if (typeof event !== 'object') {
166+
return event
167+
}
168+
169+
if (Array.isArray(event)) {
170+
return event.map((item) => sanitizeEventData(item))
171+
}
172+
173+
const sanitized: Record<string, unknown> = {}
174+
175+
for (const [key, value] of Object.entries(event)) {
176+
if (isSensitiveKey(key)) {
177+
continue
178+
}
179+
180+
if (typeof value === 'string') {
181+
sanitized[key] = isSensitiveKey(value) ? REDACTED_MARKER : redactSensitiveValues(value)
182+
} else if (Array.isArray(value)) {
183+
sanitized[key] = value.map((v) => sanitizeEventData(v))
184+
} else if (value && typeof value === 'object') {
185+
sanitized[key] = sanitizeEventData(value)
186+
} else {
187+
sanitized[key] = value
188+
}
189+
}
190+
191+
return sanitized
192+
}

0 commit comments

Comments
 (0)