Linked URL: prevent SSRF via DNS rebinding; minor fixes

This commit is contained in:
yu-i-i
2026-09-22 02:41:39 +02:00
parent 6ffb19c722
commit c1ee595528
4 changed files with 17 additions and 10 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ const server = app.listen(port, host, function (error) {
process.on('SIGTERM', () => { process.on('SIGTERM', () => {
server.close(() => { server.close(() => {
logger.info({ host, port }, 'linked-url-proxy HTTP server closed') logger.info({ host, port }, 'linked-url-proxy HTTP server closed')
metrics.close() Metrics.close()
}) })
}) })
@@ -7,6 +7,7 @@ import { Transform } from 'node:stream'
import logger from '@overleaf/logger' import logger from '@overleaf/logger'
import Settings from '@overleaf/settings' import Settings from '@overleaf/settings'
import { fetchStreamWithResponse, RequestFailedError } from '@overleaf/fetch-utils' import { fetchStreamWithResponse, RequestFailedError } from '@overleaf/fetch-utils'
import { Agent } from 'undici'
function isAllowedResource(targetUrl) { function isAllowedResource(targetUrl) {
if (!Settings.allowedResources) return false if (!Settings.allowedResources) return false
@@ -46,7 +47,7 @@ async function checkUrlAccess(hostname, targetUrl) {
throw err throw err
} }
// Permit explicitly allowed resources without checking blocked IPs // Permit explicitly allowed resources without checking blocked IPs
if (isAllowedResource(targetUrl)) return if (isAllowedResource(targetUrl)) return records[0].address
for (const { address } of records) { for (const { address } of records) {
if (isBlockedIp(address, targetUrl)) { if (isBlockedIp(address, targetUrl)) {
const err = new Error(`Blocked IP address: ${address}`) const err = new Error(`Blocked IP address: ${address}`)
@@ -54,6 +55,7 @@ async function checkUrlAccess(hostname, targetUrl) {
throw err throw err
} }
} }
return records[0].address
} }
async function validateAndFetch(rawUrl, redirectCount = 0) { async function validateAndFetch(rawUrl, redirectCount = 0) {
@@ -89,7 +91,16 @@ async function validateAndFetch(rawUrl, redirectCount = 0) {
const normalizedUrl = url.toString() const normalizedUrl = url.toString()
// check DNS and allowed resources // 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 = { const opts = {
redirect: 'manual', redirect: 'manual',
@@ -98,7 +109,7 @@ async function validateAndFetch(rawUrl, redirectCount = 0) {
} }
try { try {
const { stream, response } = await fetchStreamWithResponse(normalizedUrl, opts) const { stream, response } = await fetchStreamWithResponse(normalizedUrl, { ...opts, dispatcher: agent })
const contentLengthHeader = response.headers.get('content-length') const contentLengthHeader = response.headers.get('content-length')
if (contentLengthHeader) { if (contentLengthHeader) {
+2 -5
View File
@@ -16,10 +16,7 @@
"express": "^4.22.1", "express": "^4.22.1",
"ipaddr.js": "^2.1.0", "ipaddr.js": "^2.1.0",
"als-normalize-urlpath": "^2.3.0", "als-normalize-urlpath": "^2.3.0",
"strict-url-sanitise": "^0.0.1" "strict-url-sanitise": "^0.0.1",
}, "undici": "^7.22.0"
"devDependencies": {
"als-normalize-urlpath": "^2.3.0",
"strict-url-sanitise": "^0.0.1"
} }
} }
-1
View File
@@ -114,7 +114,6 @@
"accepts": "^1.3.7", "accepts": "^1.3.7",
"ai": "^6.0.169", "ai": "^6.0.169",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"als-normalize-urlpath": "^2.3.0",
"archiver": "^5.3.0", "archiver": "^5.3.0",
"async": "^3.2.5", "async": "^3.2.5",
"base-x": "^4.0.1", "base-x": "^4.0.1",