Skip to content

Commit ccaa49d

Browse files
waleedlatif1claude
andcommitted
fix(security): SSRF localhost hardening and regex DoS protection
Block localhost/loopback URLs in hosted environments using isHosted flag instead of allowHttp. Add safe-regex2 validation and input length limits to regex guardrails to prevent catastrophic backtracking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f1fc068 commit ccaa49d

File tree

4 files changed

+26
-14
lines changed

4 files changed

+26
-14
lines changed

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import https from 'https'
44
import type { LookupFunction } from 'net'
55
import { createLogger } from '@sim/logger'
66
import * as ipaddr from 'ipaddr.js'
7+
import { isHosted } from '@/lib/core/config/feature-flags'
78
import { type ValidationResult, validateExternalUrl } from '@/lib/core/security/input-validation'
89

910
const logger = createLogger('InputValidation')
@@ -89,10 +90,7 @@ export async function validateUrlWithDNS(
8990
return ip === '127.0.0.1' || ip === '::1'
9091
})()
9192

92-
if (
93-
isPrivateOrReservedIP(address) &&
94-
!(isLocalhost && resolvedIsLoopback && !options.allowHttp)
95-
) {
93+
if (isPrivateOrReservedIP(address) && !(isLocalhost && resolvedIsLoopback && !isHosted)) {
9694
logger.warn('URL resolves to blocked IP address', {
9795
paramName,
9896
hostname,

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -569,25 +569,25 @@ describe('validateUrlWithDNS', () => {
569569
expect(result.error).toContain('https://')
570570
})
571571

572-
it('should accept https localhost URLs', async () => {
572+
it('should accept https localhost URLs (self-hosted)', async () => {
573573
const result = await validateUrlWithDNS('https://localhost/api')
574574
expect(result.isValid).toBe(true)
575575
expect(result.resolvedIP).toBeDefined()
576576
})
577577

578-
it('should accept http localhost URLs', async () => {
578+
it('should accept http localhost URLs (self-hosted)', async () => {
579579
const result = await validateUrlWithDNS('http://localhost/api')
580580
expect(result.isValid).toBe(true)
581581
expect(result.resolvedIP).toBeDefined()
582582
})
583583

584-
it('should accept IPv4 loopback URLs', async () => {
584+
it('should accept IPv4 loopback URLs (self-hosted)', async () => {
585585
const result = await validateUrlWithDNS('http://127.0.0.1/api')
586586
expect(result.isValid).toBe(true)
587587
expect(result.resolvedIP).toBeDefined()
588588
})
589589

590-
it('should accept IPv6 loopback URLs', async () => {
590+
it('should accept IPv6 loopback URLs (self-hosted)', async () => {
591591
const result = await validateUrlWithDNS('http://[::1]/api')
592592
expect(result.isValid).toBe(true)
593593
expect(result.resolvedIP).toBeDefined()
@@ -918,7 +918,7 @@ describe('validateExternalUrl', () => {
918918
})
919919
})
920920

921-
describe('localhost and loopback addresses', () => {
921+
describe('localhost and loopback addresses (self-hosted)', () => {
922922
it.concurrent('should accept https localhost', () => {
923923
const result = validateExternalUrl('https://localhost/api')
924924
expect(result.isValid).toBe(true)
@@ -1027,7 +1027,7 @@ describe('validateImageUrl', () => {
10271027
expect(result.isValid).toBe(true)
10281028
})
10291029

1030-
it.concurrent('should accept localhost URLs', () => {
1030+
it.concurrent('should accept localhost URLs (self-hosted)', () => {
10311031
const result = validateImageUrl('https://localhost/image.png')
10321032
expect(result.isValid).toBe(true)
10331033
})

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createLogger } from '@sim/logger'
22
import * as ipaddr from 'ipaddr.js'
3+
import { isHosted } from '@/lib/core/config/feature-flags'
34

45
const logger = createLogger('InputValidation')
56

@@ -717,19 +718,26 @@ export function validateExternalUrl(
717718
error: `${paramName} must use http:// or https:// protocol`,
718719
}
719720
}
720-
if (isLocalhost) {
721+
if (isLocalhost && isHosted) {
721722
return {
722723
isValid: false,
723724
error: `${paramName} cannot point to localhost`,
724725
}
725726
}
726-
} else if (protocol !== 'https:' && !(protocol === 'http:' && isLocalhost)) {
727+
} else if (protocol !== 'https:' && !(protocol === 'http:' && isLocalhost && !isHosted)) {
727728
return {
728729
isValid: false,
729730
error: `${paramName} must use https:// protocol`,
730731
}
731732
}
732733

734+
if (isLocalhost && isHosted) {
735+
return {
736+
isValid: false,
737+
error: `${paramName} cannot point to localhost`,
738+
}
739+
}
740+
733741
if (!isLocalhost && ipaddr.isValid(cleanHostname)) {
734742
if (isPrivateOrReservedIP(cleanHostname)) {
735743
return {

apps/sim/lib/guardrails/validate_regex.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,17 @@ export interface ValidationResult {
1313
export function validateRegex(inputStr: string, pattern: string): ValidationResult {
1414
try {
1515
if (!safe(pattern)) {
16-
return { passed: false, error: 'Regex pattern rejected: potentially unsafe (catastrophic backtracking)' }
16+
return {
17+
passed: false,
18+
error: 'Regex pattern rejected: potentially unsafe (catastrophic backtracking)',
19+
}
1720
}
1821

1922
if (inputStr.length > MAX_INPUT_LENGTH) {
20-
return { passed: false, error: `Input exceeds maximum length of ${MAX_INPUT_LENGTH} characters` }
23+
return {
24+
passed: false,
25+
error: `Input exceeds maximum length of ${MAX_INPUT_LENGTH} characters`,
26+
}
2127
}
2228

2329
const regex = new RegExp(pattern)

0 commit comments

Comments
 (0)