Linked URL: prevent SSRF via DNS rebinding; minor fixes

This commit is contained in:
yu-i-i
2026-02-26 18:01:50 +01:00
parent 976716f607
commit 0737ea5b31
5 changed files with 18 additions and 12 deletions

View File

@@ -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"]

View File

@@ -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()
})
})

View File

@@ -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) {

View File

@@ -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"
}
}

View File

@@ -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",