Headline
CVE-2022-0086: improve private ip check (#3403) · transloadit/uppy@fc137e3
uppy is vulnerable to Server-Side Request Forgery (SSRF)
@@ -1,83 +1,19 @@ // eslint-disable-next-line max-classes-per-file const http = require(‘http’) const https = require(‘https’) const { URL } = require(‘url’) const dns = require(‘dns’) const ipAddress = require(‘ip-address’) const request = require(‘request’) const ipaddr = require(‘ipaddr.js’)
const logger = require(‘…/logger’)
const FORBIDDEN_IP_ADDRESS = ‘Forbidden IP address’
function isIPAddress (address) { const addressAsV6 = new ipAddress.Address6(address) const addressAsV4 = new ipAddress.Address4(address) return addressAsV6.isValid() || addressAsV4.isValid() }
/* eslint-disable max-len */ /** * Determine if a IP address provided is a private one. * Return TRUE if it’s the case, FALSE otherwise. * Excerpt from: * https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html#case-2—application-can-send-requests-to-any-external-ip-address-or-domain-name * * @param {string} ipAddress the ip address to validate * @returns {boolean} */ /* eslint-enable max-len */ function isPrivateIP (ipAddress) { let isPrivate = false // Build the list of IP prefix for V4 and V6 addresses const ipPrefix = [] // Add prefix for loopback addresses ipPrefix.push(‘127.’) ipPrefix.push(‘0.’) // Add IP V4 prefix for private addresses // See https://en.wikipedia.org/wiki/Private_network ipPrefix.push(‘10.’) ipPrefix.push(‘172.16.’) ipPrefix.push(‘172.17.’) ipPrefix.push(‘172.18.’) ipPrefix.push(‘172.19.’) ipPrefix.push(‘172.20.’) ipPrefix.push(‘172.21.’) ipPrefix.push(‘172.22.’) ipPrefix.push(‘172.23.’) ipPrefix.push(‘172.24.’) ipPrefix.push(‘172.25.’) ipPrefix.push(‘172.26.’) ipPrefix.push(‘172.27.’) ipPrefix.push(‘172.28.’) ipPrefix.push(‘172.29.’) ipPrefix.push(‘172.30.’) ipPrefix.push(‘172.31.’) ipPrefix.push(‘192.168.’) ipPrefix.push(‘169.254.’) // Add IP V6 prefix for private addresses // See https://en.wikipedia.org/wiki/Unique_local_address // See https://en.wikipedia.org/wiki/Private_network // See https://simpledns.com/private-ipv6 ipPrefix.push(‘fc’) ipPrefix.push(‘fd’) ipPrefix.push(‘fe’) ipPrefix.push(‘ff’) ipPrefix.push(‘::1’) // Verify the provided IP address // Remove whitespace characters from the beginning/end of the string // and convert it to lower case // Lower case is for preventing any IPV6 case bypass using mixed case // depending on the source used to get the IP address const ipToVerify = ipAddress.trim().toLowerCase() // Perform the check against the list of prefix for (const prefix of ipPrefix) { if (ipToVerify.startsWith(prefix)) { isPrivate = true break } }
return isPrivate } // 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
@@ -115,7 +51,7 @@ function dnsLookup (hostname, options, callback) {
const toValidate = Array.isArray(addresses) ? addresses : [{ address: addresses }] for (const record of toValidate) { if (isPrivateIP(record.address)) { if (isDisallowedIP(record.address)) { callback(new Error(FORBIDDEN_IP_ADDRESS), addresses, maybeFamily) return } @@ -127,7 +63,7 @@ function dnsLookup (hostname, options, callback) {
class HttpAgent extends http.Agent { createConnection (options, callback) { if (isIPAddress(options.host) && isPrivateIP(options.host)) { if (ipaddr.isValid(options.host) && isDisallowedIP(options.host)) { callback(new Error(FORBIDDEN_IP_ADDRESS)) return undefined } @@ -138,7 +74,7 @@ class HttpAgent extends http.Agent {
class HttpsAgent extends https.Agent { createConnection (options, callback) { if (isIPAddress(options.host) && isPrivateIP(options.host)) { if (ipaddr.isValid(options.host) && isDisallowedIP(options.host)) { callback(new Error(FORBIDDEN_IP_ADDRESS)) return undefined }