Skip to content

Commit afcbcf2

Browse files
authored
fix(tools): pin resolved IP in DB connectors to prevent DNS-rebinding SSRF (#4725)
* fix(tools): pin resolved IP in DB connectors to prevent DNS-rebinding SSRF `validateDatabaseHost` resolved an IP that was then discarded — drivers re-resolved the hostname at connect time, enabling DNS-rebinding TOCTOU. - mongodb: pass resolved IP via MongoClient `lookup` option - mysql: pin TCP socket via `stream` factory; keep hostname for TLS servername - postgresql: connect to resolved IP; pass `ssl` object with `servername` for SNI - redis: parse URL explicitly and pass options-only (URL+options breaks override due to ioredis's lodash.defaults); pin host and set `tls.servername` for rediss - neo4j: pin IP for plain `bolt://`; leave `bolt+s`/`neo4j+s` unchanged to keep Aura cert validation working (driver hardcodes servername with no override) * chore(tools): remove explainer comments from DB connector SSRF fix * fix(tools): add explicit TCP timeout to mysql stream factory * fix(tools): unify postgres ssl handling to send SNI in preferred mode * fix(tools): preserve postgres 'preferred' fallback behavior for backward compat * fix(tools): reject non-numeric Redis URL db segment instead of silently using db 0
1 parent b2ad5e9 commit afcbcf2

5 files changed

Lines changed: 56 additions & 10 deletions

File tree

apps/sim/app/api/tools/mongodb/utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { MongoClient } from 'mongodb'
2-
import { validateDatabaseHost } from '@/lib/core/security/input-validation.server'
2+
import {
3+
createPinnedLookup,
4+
validateDatabaseHost,
5+
} from '@/lib/core/security/input-validation.server'
36
import type { MongoDBCollectionInfo, MongoDBConnectionConfig } from '@/tools/mongodb/types'
47

58
export async function createMongoDBConnection(config: MongoDBConnectionConfig) {
@@ -30,6 +33,7 @@ export async function createMongoDBConnection(config: MongoDBConnectionConfig) {
3033
connectTimeoutMS: 10000,
3134
socketTimeoutMS: 10000,
3235
maxPoolSize: 1,
36+
lookup: createPinnedLookup(hostValidation.resolvedIP ?? config.host),
3337
})
3438

3539
await client.connect()

apps/sim/app/api/tools/mysql/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import net from 'node:net'
12
import mysql from 'mysql2/promise'
23
import { validateDatabaseHost } from '@/lib/core/security/input-validation.server'
34

@@ -16,12 +17,19 @@ export async function createMySQLConnection(config: MySQLConnectionConfig) {
1617
throw new Error(hostValidation.error)
1718
}
1819

20+
const resolvedIP = hostValidation.resolvedIP ?? config.host
21+
1922
const connectionConfig: mysql.ConnectionOptions = {
2023
host: config.host,
2124
port: config.port,
2225
database: config.database,
2326
user: config.username,
2427
password: config.password,
28+
stream: () => {
29+
const socket = net.connect({ host: resolvedIP, port: config.port, timeout: 10000 })
30+
socket.setNoDelay(true)
31+
return socket
32+
},
2533
}
2634

2735
if (config.ssl === 'disabled') {

apps/sim/app/api/tools/neo4j/utils.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@ export async function createNeo4jDriver(config: Neo4jConnectionConfig) {
1818
protocol = config.encryption === 'enabled' ? 'bolt+s' : 'bolt'
1919
}
2020

21-
const uri = `${protocol}://${config.host}:${config.port}`
21+
const useIPPinning = !protocol.endsWith('+s')
22+
const resolvedIP = hostValidation.resolvedIP ?? config.host
23+
const uriHost = useIPPinning
24+
? resolvedIP.includes(':')
25+
? `[${resolvedIP}]`
26+
: resolvedIP
27+
: config.host
28+
const uri = `${protocol}://${uriHost}:${config.port}`
2229

2330
const driverConfig: any = {
2431
maxConnectionPoolSize: 1,

apps/sim/app/api/tools/postgresql/utils.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@ export async function createPostgresConnection(config: PostgresConnectionConfig)
88
throw new Error(hostValidation.error)
99
}
1010

11-
const sslConfig =
11+
const resolvedHost = hostValidation.resolvedIP ?? config.host
12+
const pinIP = config.ssl !== 'preferred'
13+
14+
const sslConfig: boolean | 'prefer' | { rejectUnauthorized: boolean; servername?: string } =
1215
config.ssl === 'disabled'
1316
? false
14-
: config.ssl === 'required'
15-
? 'require'
16-
: config.ssl === 'preferred'
17-
? 'prefer'
18-
: 'require'
17+
: config.ssl === 'preferred'
18+
? 'prefer'
19+
: { rejectUnauthorized: false, servername: config.host }
1920

2021
const sql = postgres({
21-
host: config.host,
22+
host: pinIP ? resolvedHost : config.host,
2223
port: config.port,
2324
database: config.database,
2425
username: config.username,

apps/sim/app/api/tools/redis/execute/route.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,33 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
3636
return NextResponse.json({ error: hostValidation.error }, { status: 400 })
3737
}
3838

39-
client = new Redis(url, {
39+
const resolvedIP = hostValidation.resolvedIP ?? hostname
40+
const tlsEnabled = parsedUrl.protocol === 'rediss:'
41+
const port = parsedUrl.port ? Number(parsedUrl.port) : 6379
42+
const username = parsedUrl.username ? decodeURIComponent(parsedUrl.username) : undefined
43+
const password = parsedUrl.password ? decodeURIComponent(parsedUrl.password) : undefined
44+
45+
let db = 0
46+
if (parsedUrl.pathname && parsedUrl.pathname.length > 1) {
47+
const dbSegment = parsedUrl.pathname.slice(1)
48+
const parsedDb = Number.parseInt(dbSegment, 10)
49+
if (!Number.isFinite(parsedDb) || String(parsedDb) !== dbSegment) {
50+
return NextResponse.json(
51+
{ error: `Invalid Redis database index in URL path: '${dbSegment}'` },
52+
{ status: 400 }
53+
)
54+
}
55+
db = parsedDb
56+
}
57+
58+
client = new Redis({
59+
host: resolvedIP,
60+
port,
61+
username,
62+
password,
63+
db,
64+
family: resolvedIP.includes(':') ? 6 : 4,
65+
tls: tlsEnabled ? { servername: hostname } : undefined,
4066
connectTimeout: 10000,
4167
commandTimeout: 10000,
4268
maxRetriesPerRequest: 1,

0 commit comments

Comments
 (0)