Skip to content

Commit

Permalink
@uppy/companion: allow local ips when testing (transloadit#4328)
Browse files Browse the repository at this point in the history
* allow local ips when testing

in development when `allowLocalUrls` is true

* fix oops

* improve error and add test

* refactor

* add more ip block tests
  • Loading branch information
mifi authored Mar 15, 2023
1 parent 35ead7f commit 59888c0
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 44 deletions.
82 changes: 43 additions & 39 deletions src/server/helpers/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ const got = require('got').default
const logger = require('../logger')

const FORBIDDEN_IP_ADDRESS = 'Forbidden IP address'
const FORBIDDEN_RESOLVED_IP_ADDRESS = 'Forbidden resolved IP address'

// Example scary IPs that should return false (ipv6-to-ipv4 mapped):
// ::FFFF:127.0.0.1
// ::ffff:7f00:1
const isDisallowedIP = (ipAddress) => ipaddr.parse(ipAddress).range() !== 'unicast'

module.exports.FORBIDDEN_IP_ADDRESS = FORBIDDEN_IP_ADDRESS
module.exports.FORBIDDEN_RESOLVED_IP_ADDRESS = FORBIDDEN_RESOLVED_IP_ADDRESS

module.exports.getRedirectEvaluator = (rawRequestURL, isEnabled) => {
const requestURL = new URL(rawRequestURL)
Expand All @@ -41,59 +43,61 @@ module.exports.getRedirectEvaluator = (rawRequestURL, isEnabled) => {
}
}

function dnsLookup (hostname, options, callback) {
dns.lookup(hostname, options, (err, addresses, maybeFamily) => {
if (err) {
callback(err, addresses, maybeFamily)
return
}

const toValidate = Array.isArray(addresses) ? addresses : [{ address: addresses }]
for (const record of toValidate) {
if (isDisallowedIP(record.address)) {
callback(new Error(FORBIDDEN_IP_ADDRESS), addresses, maybeFamily)
/**
* Returns http Agent that will prevent requests to private IPs (to preven SSRF)
*/
const getProtectedHttpAgent = ({ protocol, blockLocalIPs }) => {
function dnsLookup (hostname, options, callback) {
dns.lookup(hostname, options, (err, addresses, maybeFamily) => {
if (err) {
callback(err, addresses, maybeFamily)
return
}
}

callback(err, addresses, maybeFamily)
})
}
const toValidate = Array.isArray(addresses) ? addresses : [{ address: addresses }]
for (const record of toValidate) {
if (blockLocalIPs && isDisallowedIP(record.address)) {
callback(new Error(FORBIDDEN_RESOLVED_IP_ADDRESS), addresses, maybeFamily)
return
}
}

class HttpAgent extends http.Agent {
createConnection (options, callback) {
if (ipaddr.isValid(options.host) && isDisallowedIP(options.host)) {
callback(new Error(FORBIDDEN_IP_ADDRESS))
return undefined
callback(err, addresses, maybeFamily)
})
}

const isBlocked = (options) => ipaddr.isValid(options.host) && blockLocalIPs && isDisallowedIP(options.host)

class HttpAgent extends http.Agent {
createConnection (options, callback) {
if (isBlocked(options)) {
callback(new Error(FORBIDDEN_IP_ADDRESS))
return undefined
}
// @ts-ignore
return super.createConnection({ ...options, lookup: dnsLookup }, callback)
}
// @ts-ignore
return super.createConnection({ ...options, lookup: dnsLookup }, callback)
}
}

class HttpsAgent extends https.Agent {
createConnection (options, callback) {
if (ipaddr.isValid(options.host) && isDisallowedIP(options.host)) {
callback(new Error(FORBIDDEN_IP_ADDRESS))
return undefined
class HttpsAgent extends https.Agent {
createConnection (options, callback) {
if (isBlocked(options)) {
callback(new Error(FORBIDDEN_IP_ADDRESS))
return undefined
}
// @ts-ignore
return super.createConnection({ ...options, lookup: dnsLookup }, callback)
}
// @ts-ignore
return super.createConnection({ ...options, lookup: dnsLookup }, callback)
}
}

/**
* Returns http Agent that will prevent requests to private IPs (to preven SSRF)
*
* @param {string} protocol http or http: or https: or https protocol needed for the request
*/
module.exports.getProtectedHttpAgent = (protocol) => {
return protocol.startsWith('https') ? HttpsAgent : HttpAgent
}

function getProtectedGot ({ url, blockLocalIPs }) {
const httpAgent = new (module.exports.getProtectedHttpAgent('http'))()
const httpsAgent = new (module.exports.getProtectedHttpAgent('https'))()
const HttpAgent = getProtectedHttpAgent({ protocol: 'http', blockLocalIPs })
const HttpsAgent = getProtectedHttpAgent({ protocol: 'https', blockLocalIPs })
const httpAgent = new HttpAgent()
const httpsAgent = new HttpsAgent()

const redirectEvaluator = module.exports.getRedirectEvaluator(url, blockLocalIPs)

Expand Down
45 changes: 40 additions & 5 deletions test/__tests__/http-agent.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const nock = require('nock')
const { getRedirectEvaluator, FORBIDDEN_IP_ADDRESS } = require('../../src/server/helpers/request')
const { getRedirectEvaluator, FORBIDDEN_IP_ADDRESS, FORBIDDEN_RESOLVED_IP_ADDRESS } = require('../../src/server/helpers/request')
const { getProtectedGot } = require('../../src/server/helpers/request')

describe('test getRedirectEvaluator', () => {
Expand Down Expand Up @@ -44,6 +44,12 @@ describe('test protected request Agent', () => {
await getProtectedGot({ url, blockLocalIPs: true }).get(url)
})

test('blocks url that resolves to forbidden IP', async () => {
const url = 'https://localhost'
const promise = getProtectedGot({ url, blockLocalIPs: true }).get(url)
await expect(promise).rejects.toThrow(new Error(FORBIDDEN_RESOLVED_IP_ADDRESS))
})

test('blocks private http IP address', async () => {
const url = 'http://172.20.10.4:8090'
const promise = getProtectedGot({ url, blockLocalIPs: true }).get(url)
Expand All @@ -56,9 +62,38 @@ describe('test protected request Agent', () => {
await expect(promise).rejects.toThrow(new Error(FORBIDDEN_IP_ADDRESS))
})

test('blocks localhost IP address', async () => {
const url = 'http://127.0.0.1:8090'
const promise = getProtectedGot({ url, blockLocalIPs: true }).get(url)
await expect(promise).rejects.toThrow(new Error(FORBIDDEN_IP_ADDRESS))
test('blocks various private IP addresses', async () => {
// eslint-disable-next-line max-len
// taken from: https://github.com/transloadit/uppy/blob/4aeef4dac0490ebb1d1fccd5582ba42c6c0fb87d/packages/%40uppy/companion/src/server/helpers/request.js#L14
const ipv4s = [
'0.0.0.0',
'0.0.0.1',
'127.0.0.1',
'127.16.0.1',
'192.168.1.1',
'169.254.1.1',
'10.0.0.1',
]

const ipv6s = [
'fd80::1234:5678:abcd:0123',
'fe80::1234:5678:abcd:0123',
'ff00::1234',
'::ffff:192.168.1.10',
'::1',
'0:0:0:0:0:0:0:1',
'fda1:3f9f:dbf7::1c8d',
]

for (const ip of ipv4s) {
const url = `http://${ip}:8090`
const promise = getProtectedGot({ url, blockLocalIPs: true }).get(url)
await expect(promise).rejects.toThrow(new Error(FORBIDDEN_IP_ADDRESS))
}
for (const ip of ipv6s) {
const url = `http://[${ip}]:8090`
const promise = getProtectedGot({ url, blockLocalIPs: true }).get(url)
await expect(promise).rejects.toThrow(new Error(FORBIDDEN_IP_ADDRESS))
}
})
})

0 comments on commit 59888c0

Please sign in to comment.