From 0737ea5b31b1ce904731489cadecfa77694f3451 Mon Sep 17 00:00:00 2001 From: yu-i-i Date: Thu, 26 Feb 2026 18:01:50 +0100 Subject: [PATCH] Linked URL: prevent SSRF via DNS rebinding; minor fixes --- develop/docker-compose.dev.yml | 2 +- services/linked-url-proxy/app.mjs | 2 +- .../app/js/LinkedUrlProxyController.mjs | 17 ++++++++++++++--- services/linked-url-proxy/package.json | 7 ++----- services/web/package.json | 2 -- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/develop/docker-compose.dev.yml b/develop/docker-compose.dev.yml index be140fd06c..dd236bc453 100644 --- a/develop/docker-compose.dev.yml +++ b/develop/docker-compose.dev.yml @@ -88,7 +88,7 @@ services: volumes: - ../services/linked-url-proxy/app:/overleaf/services/linked-url-proxy/app - ../services/linked-url-proxy/config:/overleaf/services/linked-url-proxy/config - - ../services/linked-url-proxy/app.js:/overleaf/services/linked-url-proxy/app.js + - ../services/linked-url-proxy/app.mjs:/overleaf/services/linked-url-proxy/app.mjs notifications: command: ["node", "--watch", "app.ts"] diff --git a/services/linked-url-proxy/app.mjs b/services/linked-url-proxy/app.mjs index 499a23f35e..b15caeb1c7 100644 --- a/services/linked-url-proxy/app.mjs +++ b/services/linked-url-proxy/app.mjs @@ -33,7 +33,7 @@ const server = app.listen(port, host, function (error) { process.on('SIGTERM', () => { server.close(() => { logger.info({ host, port }, 'linked-url-proxy HTTP server closed') - metrics.close() + Metrics.close() }) }) diff --git a/services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs b/services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs index ea82f65d16..cf71528741 100644 --- a/services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs +++ b/services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs @@ -7,6 +7,7 @@ import { Transform } from 'node:stream' import logger from '@overleaf/logger' import Settings from '@overleaf/settings' import { fetchStreamWithResponse, RequestFailedError } from '@overleaf/fetch-utils' +import { Agent } from 'undici' function isAllowedResource(targetUrl) { if (!Settings.allowedResources) return false @@ -46,7 +47,7 @@ async function checkUrlAccess(hostname, targetUrl) { throw err } // Permit explicitly allowed resources without checking blocked IPs - if (isAllowedResource(targetUrl)) return + if (isAllowedResource(targetUrl)) return records[0].address for (const { address } of records) { if (isBlockedIp(address, targetUrl)) { const err = new Error(`Blocked IP address: ${address}`) @@ -54,6 +55,7 @@ async function checkUrlAccess(hostname, targetUrl) { throw err } } + return records[0].address } async function validateAndFetch(rawUrl, redirectCount = 0) { @@ -89,7 +91,16 @@ async function validateAndFetch(rawUrl, redirectCount = 0) { const normalizedUrl = url.toString() // check DNS and allowed resources - await checkUrlAccess(url.hostname, normalizedUrl) + const validatedIp = await checkUrlAccess(url.hostname, normalizedUrl) + + // pin fetch to validated IP to prevent DNS rebinding + const agent = new Agent({ + connect: { + lookup(hostname, opts, cb) { + cb(null, validatedIp, 4) + } + } + }) const opts = { redirect: 'manual', @@ -98,7 +109,7 @@ async function validateAndFetch(rawUrl, redirectCount = 0) { } try { - const { stream, response } = await fetchStreamWithResponse(normalizedUrl, opts) + const { stream, response } = await fetchStreamWithResponse(normalizedUrl, { ...opts, dispatcher: agent }) const contentLengthHeader = response.headers.get('content-length') if (contentLengthHeader) { diff --git a/services/linked-url-proxy/package.json b/services/linked-url-proxy/package.json index f74c31e0e3..bc25fe4f34 100644 --- a/services/linked-url-proxy/package.json +++ b/services/linked-url-proxy/package.json @@ -16,10 +16,7 @@ "express": "^4.22.1", "ipaddr.js": "^2.1.0", "als-normalize-urlpath": "^2.3.0", - "strict-url-sanitise": "^0.0.1" - }, - "devDependencies": { - "als-normalize-urlpath": "^2.3.0", - "strict-url-sanitise": "^0.0.1" + "strict-url-sanitise": "^0.0.1", + "undici": "^7.22.0" } } diff --git a/services/web/package.json b/services/web/package.json index f1dbac3f25..09f8d8e9d9 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -110,7 +110,6 @@ "accepts": "^1.3.7", "ai": "^6.0.169", "ajv": "^8.12.0", - "als-normalize-urlpath": "^2.3.0", "archiver": "^5.3.0", "async": "^3.2.5", "base-x": "^4.0.1", @@ -184,7 +183,6 @@ "request": "2.88.2", "requestretry": "7.1.0", "sanitize-html": "^2.8.1", - "strict-url-sanitise": "^0.0.1", "stripe": "^18.4.0", "tough-cookie": "^4.0.0", "tsscmp": "^1.0.6",