Skip to content

Commit d932884

Browse files
committed
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)
1 parent 19b5099 commit d932884

5 files changed

Lines changed: 50 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: 9 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,20 @@ 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+
// Pin socket to resolved IP to prevent DNS rebinding; mysql2 still uses config.host for TLS servername.
29+
stream: () => {
30+
const socket = net.connect(config.port, resolvedIP)
31+
socket.setNoDelay(true)
32+
return socket
33+
},
2534
}
2635

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

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ 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+
// neo4j-driver hardcodes TLS servername to the URI host with no override, so we only pin the IP for non-TLS to preserve Aura cert validation.
22+
const useIPPinning = !protocol.endsWith('+s')
23+
const resolvedIP = hostValidation.resolvedIP ?? config.host
24+
const uriHost = useIPPinning
25+
? resolvedIP.includes(':')
26+
? `[${resolvedIP}]`
27+
: resolvedIP
28+
: config.host
29+
const uri = `${protocol}://${uriHost}:${config.port}`
2230

2331
const driverConfig: any = {
2432
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+
13+
// `rejectUnauthorized: false` matches postgres.js's `'require'` string semantics; `servername` is set so SNI works with the pinned IP host.
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: resolvedHost,
2223
port: config.port,
2324
database: config.database,
2425
username: config.username,

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,25 @@ 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+
const dbIndex =
45+
parsedUrl.pathname && parsedUrl.pathname.length > 1
46+
? Number.parseInt(parsedUrl.pathname.slice(1), 10)
47+
: Number.NaN
48+
49+
// Pin to resolved IP to prevent DNS rebinding; for `rediss://`, pass original hostname as TLS servername.
50+
client = new Redis({
51+
host: resolvedIP,
52+
port,
53+
username,
54+
password,
55+
db: Number.isFinite(dbIndex) ? dbIndex : 0,
56+
family: resolvedIP.includes(':') ? 6 : 4,
57+
tls: tlsEnabled ? { servername: hostname } : undefined,
4058
connectTimeout: 10000,
4159
commandTimeout: 10000,
4260
maxRetriesPerRequest: 1,

0 commit comments

Comments
 (0)