Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/sim/app/api/auth/sso/register/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { auth } from '@/lib/auth'
import { env } from '@/lib/core/config/env'
import { REDACTED_MARKER } from '@/lib/core/security/redaction'
import { createLogger } from '@/lib/logs/console/logger'

const logger = createLogger('SSO-Register')
Expand Down Expand Up @@ -236,13 +237,13 @@ export async function POST(request: NextRequest) {
oidcConfig: providerConfig.oidcConfig
? {
...providerConfig.oidcConfig,
clientSecret: '[REDACTED]',
clientSecret: REDACTED_MARKER,
}
: undefined,
samlConfig: providerConfig.samlConfig
? {
...providerConfig.samlConfig,
cert: '[REDACTED]',
cert: REDACTED_MARKER,
}
: undefined,
},
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/tools/stagehand/agent/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { env } from '@/lib/core/config/env'
import { isSensitiveKey, REDACTED_MARKER } from '@/lib/core/security/redaction'
import { createLogger } from '@/lib/logs/console/logger'
import { ensureZodObject, normalizeUrl } from '@/app/api/tools/stagehand/utils'

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

if (variablesObject && Object.keys(variablesObject).length > 0) {
const safeVarKeys = Object.keys(variablesObject).map((key) => {
return key.toLowerCase().includes('password') ? `${key}: [REDACTED]` : key
return isSensitiveKey(key) ? `${key}: ${REDACTED_MARKER}` : key
})
logger.info('Variables available for task', { variables: safeVarKeys })
}
Expand Down
34 changes: 2 additions & 32 deletions apps/sim/instrumentation-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

import { env } from './lib/core/config/env'
import { sanitizeEventData } from './lib/core/security/redaction'

if (typeof window !== 'undefined') {
const TELEMETRY_STATUS_KEY = 'simstudio-telemetry-status'
Expand Down Expand Up @@ -41,37 +42,6 @@ if (typeof window !== 'undefined') {
}
}

/**
* Sanitize event data to remove sensitive information
*/
function sanitizeEvent(event: any): any {
const patterns = ['password', 'token', 'secret', 'key', 'auth', 'credential', 'private']
const sensitiveRe = new RegExp(patterns.join('|'), 'i')

const scrubString = (s: string) => (s && sensitiveRe.test(s) ? '[redacted]' : s)

if (event == null) return event
if (typeof event === 'string') return scrubString(event)
if (typeof event !== 'object') return event

if (Array.isArray(event)) {
return event.map((item) => sanitizeEvent(item))
}

const sanitized: Record<string, unknown> = {}
for (const [key, value] of Object.entries(event)) {
const lowerKey = key.toLowerCase()
if (patterns.some((p) => lowerKey.includes(p))) continue

if (typeof value === 'string') sanitized[key] = scrubString(value)
else if (Array.isArray(value)) sanitized[key] = value.map((v) => sanitizeEvent(v))
else if (value && typeof value === 'object') sanitized[key] = sanitizeEvent(value)
else sanitized[key] = value
}

return sanitized
}

/**
* Flush batch of events to server
*/
Expand All @@ -84,7 +54,7 @@ if (typeof window !== 'undefined') {
batchTimer = null
}

const sanitizedBatch = batch.map(sanitizeEvent)
const sanitizedBatch = batch.map(sanitizeEventData)

const payload = JSON.stringify({
category: 'batch',
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/core/security/input-validation.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { describe, expect, it } from 'vitest'
import {
createPinnedUrl,
sanitizeForLogging,
validateAlphanumericId,
validateEnum,
validateFileExtension,
Expand All @@ -11,6 +10,7 @@ import {
validateUrlWithDNS,
validateUUID,
} from '@/lib/core/security/input-validation'
import { sanitizeForLogging } from '@/lib/core/security/redaction'

describe('validatePathSegment', () => {
describe('valid inputs', () => {
Expand Down
23 changes: 0 additions & 23 deletions apps/sim/lib/core/security/input-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -556,29 +556,6 @@ export function validateFileExtension(
return { isValid: true, sanitized: normalizedExt }
}

/**
* Sanitizes a string for safe logging (removes potential sensitive data patterns)
*
* @param value - The value to sanitize
* @param maxLength - Maximum length to return (default: 100)
* @returns Sanitized string safe for logging
*/
export function sanitizeForLogging(value: string, maxLength = 100): string {
if (!value) return ''

// Truncate long values
let sanitized = value.substring(0, maxLength)

// Mask common sensitive patterns
sanitized = sanitized
.replace(/Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, 'Bearer [REDACTED]')
.replace(/password['":\s]*['"]\w+['"]/gi, 'password: "[REDACTED]"')
.replace(/token['":\s]*['"]\w+['"]/gi, 'token: "[REDACTED]"')
.replace(/api[_-]?key['":\s]*['"]\w+['"]/gi, 'api_key: "[REDACTED]"')

return sanitized
}

/**
* Validates Microsoft Graph API resource IDs
*
Expand Down
186 changes: 172 additions & 14 deletions apps/sim/lib/core/security/redaction.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,122 @@
/**
* Recursively redacts API keys in an object
* @param obj The object to redact API keys from
* @returns A new object with API keys redacted
* Centralized redaction utilities for sensitive data
*/
export const redactApiKeys = (obj: any): any => {
if (!obj || typeof obj !== 'object') {

/** Standard marker used for all redacted values */
export const REDACTED_MARKER = '[REDACTED]'

/**
* Patterns for sensitive key names (case-insensitive matching)
* These patterns match common naming conventions for sensitive data
*/
const SENSITIVE_KEY_PATTERNS: RegExp[] = [
/^api[_-]?key$/i,
/^access[_-]?token$/i,
/^refresh[_-]?token$/i,
/^client[_-]?secret$/i,
/^private[_-]?key$/i,
/^auth[_-]?token$/i,
/\bsecret\b/i,
/\bpassword\b/i,
/\btoken\b/i,
/\bcredential\b/i,
/\bauthorization\b/i,
/\bbearer\b/i,
/\bprivate\b/i,
/\bauth\b/i,
]

/**
* Patterns for sensitive values in strings (for redacting values, not keys)
* Each pattern has a replacement function
*/
const SENSITIVE_VALUE_PATTERNS: Array<{
pattern: RegExp
replacement: string
}> = [
// Bearer tokens
{
pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi,
replacement: `Bearer ${REDACTED_MARKER}`,
},
// Basic auth
{
pattern: /Basic\s+[A-Za-z0-9+/]+=*/gi,
replacement: `Basic ${REDACTED_MARKER}`,
},
// API keys that look like sk-..., pk-..., etc.
{
pattern: /\b(sk|pk|api|key)[_-][A-Za-z0-9\-._]{20,}\b/gi,
replacement: REDACTED_MARKER,
},
// JSON-style password fields: password: "value" or password: 'value'
{
pattern: /password['":\s]*['"][^'"]+['"]/gi,
replacement: `password: "${REDACTED_MARKER}"`,
},
// JSON-style token fields: token: "value" or token: 'value'
{
pattern: /token['":\s]*['"][^'"]+['"]/gi,
replacement: `token: "${REDACTED_MARKER}"`,
},
// JSON-style api_key fields: api_key: "value" or api-key: "value"
{
pattern: /api[_-]?key['":\s]*['"][^'"]+['"]/gi,
replacement: `api_key: "${REDACTED_MARKER}"`,
},
]

/**
* Checks if a key name matches any sensitive pattern
* @param key - The key name to check
* @returns True if the key is considered sensitive
*/
export function isSensitiveKey(key: string): boolean {
const lowerKey = key.toLowerCase()
return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(lowerKey))
}

/**
* Redacts sensitive patterns from a string value
* @param value - The string to redact
* @returns The string with sensitive patterns redacted
*/
export function redactSensitiveValues(value: string): string {
if (!value || typeof value !== 'string') {
return value
}

let result = value
for (const { pattern, replacement } of SENSITIVE_VALUE_PATTERNS) {
result = result.replace(pattern, replacement)
}
return result
}

/**
* Recursively redacts sensitive data (API keys, passwords, tokens, etc.) from an object
*
* @param obj - The object to redact sensitive data from
* @returns A new object with sensitive data redacted
*/
export function redactApiKeys(obj: any): any {
if (obj === null || obj === undefined) {
return obj
}

if (typeof obj !== 'object') {
return obj
}

if (Array.isArray(obj)) {
return obj.map(redactApiKeys)
return obj.map((item) => redactApiKeys(item))
}

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

for (const [key, value] of Object.entries(obj)) {
if (
key.toLowerCase() === 'apikey' ||
key.toLowerCase() === 'api_key' ||
key.toLowerCase() === 'access_token' ||
/\bsecret\b/i.test(key.toLowerCase()) ||
/\bpassword\b/i.test(key.toLowerCase())
) {
result[key] = '***REDACTED***'
if (isSensitiveKey(key)) {
result[key] = REDACTED_MARKER
} else if (typeof value === 'object' && value !== null) {
result[key] = redactApiKeys(value)
} else {
Expand All @@ -32,3 +126,67 @@ export const redactApiKeys = (obj: any): any => {

return result
}

/**
* Sanitizes a string for safe logging by truncating and redacting sensitive patterns
*
* @param value - The string to sanitize
* @param maxLength - Maximum length of the output (default: 100)
* @returns The sanitized string
*/
export function sanitizeForLogging(value: string, maxLength = 100): string {
if (!value) return ''

let sanitized = value.substring(0, maxLength)

sanitized = redactSensitiveValues(sanitized)

return sanitized
}

/**
* Sanitizes event data for error reporting/analytics
*
* @param event - The event data to sanitize
* @returns Sanitized event data safe for external reporting
*/
export function sanitizeEventData(event: any): any {
if (event === null || event === undefined) {
return event
}

if (typeof event === 'string') {
if (isSensitiveKey(event)) {
return REDACTED_MARKER
}
return redactSensitiveValues(event)
}

if (typeof event !== 'object') {
return event
}

if (Array.isArray(event)) {
return event.map((item) => sanitizeEventData(item))
}

const sanitized: Record<string, unknown> = {}

for (const [key, value] of Object.entries(event)) {
if (isSensitiveKey(key)) {
continue
}

if (typeof value === 'string') {
sanitized[key] = isSensitiveKey(value) ? REDACTED_MARKER : redactSensitiveValues(value)
} else if (Array.isArray(value)) {
sanitized[key] = value.map((v) => sanitizeEventData(v))
} else if (value && typeof value === 'object') {
sanitized[key] = sanitizeEventData(value)
} else {
sanitized[key] = value
}
}

return sanitized
}
Loading
Loading