mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import { MongoClient } from 'mongodb'
|
||||
import { validateDatabaseHost } from '@/lib/core/security/input-validation.server'
|
||||
import {
|
||||
createPinnedLookup,
|
||||
validateDatabaseHost,
|
||||
} from '@/lib/core/security/input-validation.server'
|
||||
import type { MongoDBCollectionInfo, MongoDBConnectionConfig } from '@/tools/mongodb/types'
|
||||
|
||||
export async function createMongoDBConnection(config: MongoDBConnectionConfig) {
|
||||
@@ -30,6 +33,7 @@ export async function createMongoDBConnection(config: MongoDBConnectionConfig) {
|
||||
connectTimeoutMS: 10000,
|
||||
socketTimeoutMS: 10000,
|
||||
maxPoolSize: 1,
|
||||
lookup: createPinnedLookup(hostValidation.resolvedIP ?? config.host),
|
||||
})
|
||||
|
||||
await client.connect()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import net from 'node:net'
|
||||
import mysql from 'mysql2/promise'
|
||||
import { validateDatabaseHost } from '@/lib/core/security/input-validation.server'
|
||||
|
||||
@@ -16,12 +17,19 @@ export async function createMySQLConnection(config: MySQLConnectionConfig) {
|
||||
throw new Error(hostValidation.error)
|
||||
}
|
||||
|
||||
const resolvedIP = hostValidation.resolvedIP ?? config.host
|
||||
|
||||
const connectionConfig: mysql.ConnectionOptions = {
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
database: config.database,
|
||||
user: config.username,
|
||||
password: config.password,
|
||||
stream: () => {
|
||||
const socket = net.connect({ host: resolvedIP, port: config.port, timeout: 10000 })
|
||||
socket.setNoDelay(true)
|
||||
return socket
|
||||
},
|
||||
}
|
||||
|
||||
if (config.ssl === 'disabled') {
|
||||
|
||||
@@ -18,7 +18,14 @@ export async function createNeo4jDriver(config: Neo4jConnectionConfig) {
|
||||
protocol = config.encryption === 'enabled' ? 'bolt+s' : 'bolt'
|
||||
}
|
||||
|
||||
const uri = `${protocol}://${config.host}:${config.port}`
|
||||
const useIPPinning = !protocol.endsWith('+s')
|
||||
const resolvedIP = hostValidation.resolvedIP ?? config.host
|
||||
const uriHost = useIPPinning
|
||||
? resolvedIP.includes(':')
|
||||
? `[${resolvedIP}]`
|
||||
: resolvedIP
|
||||
: config.host
|
||||
const uri = `${protocol}://${uriHost}:${config.port}`
|
||||
|
||||
const driverConfig: any = {
|
||||
maxConnectionPoolSize: 1,
|
||||
|
||||
@@ -8,17 +8,18 @@ export async function createPostgresConnection(config: PostgresConnectionConfig)
|
||||
throw new Error(hostValidation.error)
|
||||
}
|
||||
|
||||
const sslConfig =
|
||||
const resolvedHost = hostValidation.resolvedIP ?? config.host
|
||||
const pinIP = config.ssl !== 'preferred'
|
||||
|
||||
const sslConfig: boolean | 'prefer' | { rejectUnauthorized: boolean; servername?: string } =
|
||||
config.ssl === 'disabled'
|
||||
? false
|
||||
: config.ssl === 'required'
|
||||
? 'require'
|
||||
: config.ssl === 'preferred'
|
||||
? 'prefer'
|
||||
: 'require'
|
||||
: config.ssl === 'preferred'
|
||||
? 'prefer'
|
||||
: { rejectUnauthorized: false, servername: config.host }
|
||||
|
||||
const sql = postgres({
|
||||
host: config.host,
|
||||
host: pinIP ? resolvedHost : config.host,
|
||||
port: config.port,
|
||||
database: config.database,
|
||||
username: config.username,
|
||||
|
||||
@@ -36,7 +36,33 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
return NextResponse.json({ error: hostValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
client = new Redis(url, {
|
||||
const resolvedIP = hostValidation.resolvedIP ?? hostname
|
||||
const tlsEnabled = parsedUrl.protocol === 'rediss:'
|
||||
const port = parsedUrl.port ? Number(parsedUrl.port) : 6379
|
||||
const username = parsedUrl.username ? decodeURIComponent(parsedUrl.username) : undefined
|
||||
const password = parsedUrl.password ? decodeURIComponent(parsedUrl.password) : undefined
|
||||
|
||||
let db = 0
|
||||
if (parsedUrl.pathname && parsedUrl.pathname.length > 1) {
|
||||
const dbSegment = parsedUrl.pathname.slice(1)
|
||||
const parsedDb = Number.parseInt(dbSegment, 10)
|
||||
if (!Number.isFinite(parsedDb) || String(parsedDb) !== dbSegment) {
|
||||
return NextResponse.json(
|
||||
{ error: `Invalid Redis database index in URL path: '${dbSegment}'` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
db = parsedDb
|
||||
}
|
||||
|
||||
client = new Redis({
|
||||
host: resolvedIP,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
db,
|
||||
family: resolvedIP.includes(':') ? 6 : 4,
|
||||
tls: tlsEnabled ? { servername: hostname } : undefined,
|
||||
connectTimeout: 10000,
|
||||
commandTimeout: 10000,
|
||||
maxRetriesPerRequest: 1,
|
||||
|
||||
Reference in New Issue
Block a user