Skip to content

Commit e298899

Browse files
committed
fix(security): remove extraneous comments and fix failing SSRF test
1 parent 816b4a0 commit e298899

File tree

4 files changed

+10
-27
lines changed

4 files changed

+10
-27
lines changed

apps/sim/app/api/function/execute/route.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ describe('Function Execute API Route', () => {
211211

212212
it.concurrent('should block SSRF attacks through secure fetch wrapper', async () => {
213213
expect(validateProxyUrl('http://169.254.169.254/latest/meta-data/').isValid).toBe(false)
214-
expect(validateProxyUrl('http://127.0.0.1:8080/admin').isValid).toBe(false)
214+
expect(validateProxyUrl('http://127.0.0.1:8080/admin').isValid).toBe(true)
215215
expect(validateProxyUrl('http://192.168.1.1/config').isValid).toBe(false)
216216
expect(validateProxyUrl('http://10.0.0.1/internal').isValid).toBe(false)
217217
})

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ export async function validateUrlWithDNS(
6565
const hostname = parsedUrl.hostname
6666

6767
try {
68-
const lookupHostname = hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname
68+
const lookupHostname =
69+
hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname
6970
const { address } = await dns.lookup(lookupHostname, { verbatim: true })
7071

7172
const hostnameLower = hostname.toLowerCase()
@@ -201,8 +202,6 @@ export async function secureFetchWithPinnedIP(
201202

202203
const agent = isHttps ? new https.Agent(agentOptions) : new http.Agent(agentOptions)
203204

204-
// Remove accept-encoding since Node.js http/https doesn't auto-decompress
205-
// Headers are lowercase due to Web Headers API normalization in executeToolRequest
206205
const { 'accept-encoding': _, ...sanitizedHeaders } = options.headers ?? {}
207206

208207
const requestOptions: http.RequestOptions = {
@@ -212,7 +211,7 @@ export async function secureFetchWithPinnedIP(
212211
method: options.method || 'GET',
213212
headers: sanitizedHeaders,
214213
agent,
215-
timeout: options.timeout || 300000, // Default 5 minutes
214+
timeout: options.timeout || 300000,
216215
}
217216

218217
const protocol = isHttps ? https : http

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -916,7 +916,6 @@ describe('validateExternalUrl', () => {
916916
expect(result.isValid).toBe(false)
917917
expect(result.error).toContain('valid URL')
918918
})
919-
920919
})
921920

922921
describe('localhost and loopback addresses', () => {

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

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ export function validatePathSegment(
8989
const pathTraversalPatterns = [
9090
'..',
9191
'./',
92-
'.\\.', // Windows path traversal
93-
'%2e%2e', // URL encoded ..
94-
'%252e%252e', // Double URL encoded ..
92+
'.\\.',
93+
'%2e%2e',
94+
'%252e%252e',
9595
'..%2f',
9696
'..%5c',
9797
'%2e%2e%2f',
@@ -391,7 +391,6 @@ export function validateHostname(
391391

392392
const lowerHostname = hostname.toLowerCase()
393393

394-
// Block localhost
395394
if (lowerHostname === 'localhost') {
396395
logger.warn('Hostname is localhost', { paramName })
397396
return {
@@ -400,7 +399,6 @@ export function validateHostname(
400399
}
401400
}
402401

403-
// Use ipaddr.js to check if hostname is an IP and if it's private/reserved
404402
if (ipaddr.isValid(lowerHostname)) {
405403
if (isPrivateOrReservedIP(lowerHostname)) {
406404
logger.warn('Hostname matches blocked IP range', {
@@ -414,7 +412,6 @@ export function validateHostname(
414412
}
415413
}
416414

417-
// Basic hostname format validation
418415
const hostnamePattern =
419416
/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i
420417

@@ -460,10 +457,7 @@ export function validateFileExtension(
460457
}
461458
}
462459

463-
// Remove leading dot if present
464460
const ext = extension.startsWith('.') ? extension.slice(1) : extension
465-
466-
// Normalize to lowercase
467461
const normalizedExt = ext.toLowerCase()
468462

469463
if (!allowedExtensions.map((e) => e.toLowerCase()).includes(normalizedExt)) {
@@ -515,7 +509,6 @@ export function validateMicrosoftGraphId(
515509
}
516510
}
517511

518-
// Check for path traversal patterns (../)
519512
const pathTraversalPatterns = [
520513
'../',
521514
'..\\',
@@ -525,7 +518,7 @@ export function validateMicrosoftGraphId(
525518
'%2e%2e%5c',
526519
'%2e%2e\\',
527520
'..%5c',
528-
'%252e%252e%252f', // double encoded
521+
'%252e%252e%252f',
529522
]
530523

531524
const lowerValue = value.toLowerCase()
@@ -542,7 +535,6 @@ export function validateMicrosoftGraphId(
542535
}
543536
}
544537

545-
// Check for control characters and null bytes
546538
if (/[\x00-\x1f\x7f]/.test(value) || value.includes('%00')) {
547539
logger.warn('Control characters in Microsoft Graph ID', { paramName })
548540
return {
@@ -551,16 +543,13 @@ export function validateMicrosoftGraphId(
551543
}
552544
}
553545

554-
// Check for newlines (which could be used for header injection)
555546
if (value.includes('\n') || value.includes('\r')) {
556547
return {
557548
isValid: false,
558549
error: `${paramName} contains invalid newline characters`,
559550
}
560551
}
561552

562-
// Microsoft Graph IDs can contain many characters, but not suspicious patterns
563-
// We've blocked path traversal, so allow the rest
564553
return { isValid: true, sanitized: value }
565554
}
566555

@@ -583,7 +572,6 @@ export function validateJiraCloudId(
583572
value: string | null | undefined,
584573
paramName = 'cloudId'
585574
): ValidationResult {
586-
// Jira cloud IDs are alphanumeric with hyphens (UUID-like)
587575
return validatePathSegment(value, {
588576
paramName,
589577
allowHyphens: true,
@@ -612,7 +600,6 @@ export function validateJiraIssueKey(
612600
value: string | null | undefined,
613601
paramName = 'issueKey'
614602
): ValidationResult {
615-
// Jira issue keys: letters, numbers, hyphens (PROJECT-123 format)
616603
return validatePathSegment(value, {
617604
paramName,
618605
allowHyphens: true,
@@ -653,7 +640,6 @@ export function validateExternalUrl(
653640
}
654641
}
655642

656-
// Must be a valid URL
657643
let parsedUrl: URL
658644
try {
659645
parsedUrl = new URL(url)
@@ -667,7 +653,8 @@ export function validateExternalUrl(
667653
const protocol = parsedUrl.protocol
668654
const hostname = parsedUrl.hostname.toLowerCase()
669655

670-
const cleanHostname = hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname
656+
const cleanHostname =
657+
hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname
671658

672659
let isLocalhost = cleanHostname === 'localhost'
673660
if (ipaddr.isValid(cleanHostname)) {
@@ -677,15 +664,13 @@ export function validateExternalUrl(
677664
}
678665
}
679666

680-
// Require HTTPS except for localhost development
681667
if (protocol !== 'https:' && !(protocol === 'http:' && isLocalhost)) {
682668
return {
683669
isValid: false,
684670
error: `${paramName} must use https:// protocol`,
685671
}
686672
}
687673

688-
// Block private/reserved IPs while allowing loopback addresses for local development.
689674
if (!isLocalhost && ipaddr.isValid(cleanHostname)) {
690675
if (isPrivateOrReservedIP(cleanHostname)) {
691676
return {

0 commit comments

Comments
 (0)