mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 09:09:36 +02:00
Linked URL: prevent SSRF via DNS rebinding; minor fixes
This commit is contained in:
@@ -88,7 +88,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ../services/linked-url-proxy/app:/overleaf/services/linked-url-proxy/app
|
- ../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/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:
|
notifications:
|
||||||
command: ["node", "--watch", "app.ts"]
|
command: ["node", "--watch", "app.ts"]
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,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",
|
||||||
@@ -184,7 +183,6 @@
|
|||||||
"request": "2.88.2",
|
"request": "2.88.2",
|
||||||
"requestretry": "7.1.0",
|
"requestretry": "7.1.0",
|
||||||
"sanitize-html": "^2.8.1",
|
"sanitize-html": "^2.8.1",
|
||||||
"strict-url-sanitise": "^0.0.1",
|
|
||||||
"stripe": "^18.4.0",
|
"stripe": "^18.4.0",
|
||||||
"tough-cookie": "^4.0.0",
|
"tough-cookie": "^4.0.0",
|
||||||
"tsscmp": "^1.0.6",
|
"tsscmp": "^1.0.6",
|
||||||
|
|||||||
Reference in New Issue
Block a user