CVE-2022-0086: Server-Side Request Forgery (SSRF) (CWE-918)
value: uppy is vulnerable to Server-Side Request Forgery (SSRF)
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 9.8
Published: 2022-01-04T18:15Z
Repository: https://github.com/transloadit/uppy
Language: JSON
Files changed: packages/@uppy/companion/package.json, packages/@uppy/companion/src/server/helpers/request.js, yarn.lock
Commit date: 2022-01-04 17:02:44 +0700
Commit message: improve private ip check (#3403)

https://huntr.dev/bounties/c1c03ef6-3f18-4976-a9ad-08c251279122/
Diff: diff --git a/packages/@uppy/companion/package.json b/packages/@uppy/companion/package.json
index de1701087..153c72799 100644
--- a/packages/@uppy/companion/package.json
+++ b/packages/@uppy/companion/package.json
@@ -43,11 +43,11 @@
     "express-prom-bundle": "6.3.0",
     "express-request-id": "1.4.1",
     "express-session": "1.17.1",
     "grant": "4.7.0",
     "helmet": "^4.6.0",
-    "ip-address": "6.2.0",
+    "ipaddr.js": "^2.0.1",
     "isobject": "3.0.1",
     "jsonwebtoken": "8.5.1",
     "lodash.merge": "^4.6.2",
     "mime-types": "2.1.25",
     "moment": "^2.29.1",
diff --git a/packages/@uppy/companion/src/server/helpers/request.js b/packages/@uppy/companion/src/server/helpers/request.js
index bdd5bb4a2..48125a968 100644
--- a/packages/@uppy/companion/src/server/helpers/request.js
+++ b/packages/@uppy/companion/src/server/helpers/request.js
@@ -1,85 +1,21 @@
+// 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
 
 module.exports.getRedirectEvaluator = (rawRequestURL, blockPrivateIPs) => {
   const requestURL = new URL(rawRequestURL)
@@ -113,11 +49,11 @@ function dnsLookup (hostname, options, callback) {
       return
     }
 
     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
       }
     }
 
@@ -125,22 +61,22 @@ 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
     }
     // @ts-ignore
     return super.createConnection({ ...options, lookup: dnsLookup }, callback)
   }
 }
 
 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
     }
     // @ts-ignore
     return super.createConnection({ ...options, lookup: dnsLookup }, callback)
diff --git a/yarn.lock b/yarn.lock
index bb517c574..e85818443 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8098,11 +8098,11 @@ __metadata:
     express-request-id: 1.4.1
     express-session: 1.17.1
     grant: 4.7.0
     helmet: ^4.6.0
     into-stream: ^6.0.0
-    ip-address: 6.2.0
+    ipaddr.js: ^2.0.1
     isobject: 3.0.1
     jsonwebtoken: 8.5.1
     lodash.merge: ^4.6.2
     mime-types: 2.1.25
     moment: ^2.29.1
@@ -23123,25 +23123,10 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
     loose-envify: ^1.0.0
   checksum: cc3182d793aad82a8d1f0af697b462939cb46066ec48bbf1707c150ad5fad6406137e91a262022c269702e01621f35ef60269f6c0d7fd178487959809acdfb14
   languageName: node
   linkType: hard
 
-"ip-address@npm:6.2.0":
-  version: 6.2.0
-  resolution: "ip-address@npm:6.2.0"
-  dependencies:
-    jsbn: 1.1.0
-    lodash.find: 4.6.0
-    lodash.max: 4.0.1
-    lodash.merge: 4.6.2
-    lodash.padstart: 4.6.1
-    lodash.repeat: 4.1.0
-    sprintf-js: 1.1.2
-  checksum: bcb93b4b13b3bf17d033fafce45850bf14c56366659ab627d3adbcd7a87ec64e219e7c6dc5cf990dddff5ac59db76520f2b1c7b5b05ea8d069162ca8a5112e44
-  languageName: node
-  linkType: hard
-
 "ip-regex@npm:^2.1.0":
   version: 2.1.0
   resolution: "ip-regex@npm:2.1.0"
   checksum: 331d95052aa53ce245745ea0fc3a6a1e2e3c8d6da65fa8ea52bf73768c1b22a9ac50629d1d2b08c04e7b3ac4c21b536693c149ce2c2615ee4796030e5b3e3cba
   languageName: node
@@ -25045,17 +25030,10 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
     js-yaml: bin/js-yaml.js
   checksum: bef146085f472d44dee30ec34e5cf36bf89164f5d585435a3d3da89e52622dff0b188a580e4ad091c3341889e14cb88cac6e4deb16dc5b1e9623bb0601fc255c
   languageName: node
   linkType: hard
 
-"jsbn@npm:1.1.0":
-  version: 1.1.0
-  resolution: "jsbn@npm:1.1.0"
-  checksum: 944f924f2bd67ad533b3850eee47603eed0f6ae425fd1ee8c760f477e8c34a05f144c1bd4f5a5dd1963141dc79a2c55f89ccc5ab77d039e7077f3ad196b64965
-  languageName: node
-  linkType: hard
-
 "jsbn@npm:~0.1.0":
   version: 0.1.1
   resolution: "jsbn@npm:0.1.1"
   checksum: e5ff29c1b8d965017ef3f9c219dacd6e40ad355c664e277d31246c90545a02e6047018c16c60a00f36d561b3647215c41894f5d869ada6908a2e0ce4200c88f2
   languageName: node
@@ -26429,17 +26407,10 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
   resolution: "lodash.escape@npm:4.0.1"
   checksum: fcb54f457497256964d619d5cccbd80a961916fca60df3fe0fa3e7f052715c2944c0ed5aefb4f9e047d127d44aa2d55555f3350cb42c6549e9e293fb30b41e7f
   languageName: node
   linkType: hard
 
-"lodash.find@npm:4.6.0":
-  version: 4.6.0
-  resolution: "lodash.find@npm:4.6.0"
-  checksum: b737f849a4fe36f5c3664ea636780dda2fde18335021faf80cdfdcb300ed75441da6f55cfd6de119092d8bb2ddbc4433f4a8de4b99c0b9c8640465b0901c717c
-  languageName: node
-  linkType: hard
-
 "lodash.flatten@npm:^4.4.0":
   version: 4.4.0
   resolution: "lodash.flatten@npm:4.4.0"
   checksum: 0ac34a393d4b795d4b7421153d27c13ae67e08786c9cbb60ff5b732210d46f833598eee3fb3844bb10070e8488efe390ea53bb567377e0cb47e9e630bf0811cb
   languageName: node
@@ -26541,17 +26512,10 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
   resolution: "lodash.mapvalues@npm:4.6.0"
   checksum: 0ff1b252fda318fc36e47c296984925e98fbb0fc5a2ecc4ef458f3c739a9476d47e40c95ac653e8314d132aa59c746d4276527b99d6e271940555c6e12d2babd
   languageName: node
   linkType: hard
 
-"lodash.max@npm:4.0.1":
-  version: 4.0.1
-  resolution: "lodash.max@npm:4.0.1"
-  checksum: f887b68db054edabe3a4f4877a7b7d0bef6e4a29e9371ea4d711a6ed0ca2393659a879158c609649aabd0337528ec58ed5b1e9581fed536ec0b8ae45a6e5f278
-  languageName: node
-  linkType: hard
-
 "lodash.memoize@npm:4.1.2, lodash.memoize@npm:^4.1.2":
   version: 4.1.2
   resolution: "lodash.memoize@npm:4.1.2"
   checksum: 9ff3942feeccffa4f1fafa88d32f0d24fdc62fd15ded5a74a5f950ff5f0c6f61916157246744c620173dddf38d37095a92327d5fd3861e2063e736a5c207d089
   languageName: node
@@ -26562,11 +26526,11 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
   resolution: "lodash.memoize@npm:3.0.4"
   checksum: fc52e0916b896fa79d6b85fbeaa0e44a381b70f1fcab7acab10188aaeeb2107e21b9b992bff560f405696e0a6e3bb5c08af18955d628a1e8ab6b11df14ff6172
   languageName: node
   linkType: hard
 
-"lodash.merge@npm:4.6.2, lodash.merge@npm:^4.6.1, lodash.merge@npm:^4.6.2":
+"lodash.merge@npm:^4.6.1, lodash.merge@npm:^4.6.2":
   version: 4.6.2
   resolution: "lodash.merge@npm:4.6.2"
   checksum: ad580b4bdbb7ca1f7abf7e1bce63a9a0b98e370cf40194b03380a46b4ed799c9573029599caebc1b14e3f24b111aef72b96674a56cfa105e0f5ac70546cdc005
   languageName: node
   linkType: hard
@@ -26583,17 +26547,10 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
   resolution: "lodash.once@npm:4.1.1"
   checksum: d768fa9f9b4e1dc6453be99b753906f58990e0c45e7b2ca5a3b40a33111e5d17f6edf2f768786e2716af90a8e78f8f91431ab8435f761fef00f9b0c256f6d245
   languageName: node
   linkType: hard
 
-"lodash.padstart@npm:4.6.1":
-  version: 4.6.1
-  resolution: "lodash.padstart@npm:4.6.1"
-  checksum: 0d6ad92c626d351db85de539e41df3238d7d36c5fbfc5f57c4f060c90c73ad9f1db566463487795fdf0bf290a8f133189a0bd91d051032f6eb2d15b7e1863b5e
-  languageName: node
-  linkType: hard
-
 "lodash.pick@npm:^4.4.0":
   version: 4.4.0
   resolution: "lodash.pick@npm:4.4.0"
   checksum: 2c36cab7da6b999a20bd3373b40e31a3ef81fa264f34a6979c852c5bc8ac039379686b27380f0cb8e3781610844fafec6949c6fbbebc059c98f8fa8570e3675f
   languageName: node
@@ -26604,17 +26561,10 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
   resolution: "lodash.pickby@npm:4.6.0"
   checksum: a554d898c15bcd3218e4005b95b5146210bd862010c7d242d17106ee36aed9b9209a858ce974136ab1faadd86a82297761c206fda7f1886278bac827145c5536
   languageName: node
   linkType: hard
 
-"lodash.repeat@npm:4.1.0":
-  version: 4.1.0
-  resolution: "lodash.repeat@npm:4.1.0"
-  checksum: dac15fc59ed783678e1a9f986fefa180bfdbf95280852165965ecc8e15b871c6f0eaf7b325768a176014594d5186f1d6558fb72a18527bddd82539fb3ef8a4d3
-  languageName: node
-  linkType: hard
-
 "lodash.set@npm:^4.3.2":
   version: 4.3.2
   resolution: "lodash.set@npm:4.3.2"
   checksum: a9122f49eef9f2d0fc9061a33d87f8e5b8c6b23d46e8b9e9ce1529d3588d79741bd1145a3abdfa3b13082703e65af27ff18d8a07bfc22b9be32f3fc36f763f70
   languageName: node
@@ -37699,11 +37649,11 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
   resolution: "split2@npm:4.1.0"
   checksum: ec581597cb74c13cdfb5e2047543dd40cb1e8e9803c7b1e0c29ede05f2b4f049b2d6e7f2788a225d544549375719658b8f38e9366364dec35dc7a12edfda5ee5
   languageName: node
   linkType: hard
 
-"sprintf-js@npm:1.1.2, sprintf-js@npm:^1.0.3, sprintf-js@npm:^1.1.2":
+"sprintf-js@npm:^1.0.3, sprintf-js@npm:^1.1.2":
   version: 1.1.2
   resolution: "sprintf-js@npm:1.1.2"
   checksum: d4bb46464632b335e5faed381bd331157e0af64915a98ede833452663bc672823db49d7531c32d58798e85236581fb7342fd0270531ffc8f914e186187bf1c90
   languageName: node
   linkType: hard

Vulnerable code:     "ip-address": "6.2.0",
const ipAddress = require('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
}
      if (isPrivateIP(record.address)) {
    if (isIPAddress(options.host) && isPrivateIP(options.host)) {
    if (isIPAddress(options.host) && isPrivateIP(options.host)) {
    ip-address: 6.2.0
"ip-address@npm:6.2.0":
  version: 6.2.0
  resolution: "ip-address@npm:6.2.0"
  dependencies:
    jsbn: 1.1.0
    lodash.find: 4.6.0
    lodash.max: 4.0.1
    lodash.merge: 4.6.2
    lodash.padstart: 4.6.1
    lodash.repeat: 4.1.0
    sprintf-js: 1.1.2
  checksum: bcb93b4b13b3bf17d033fafce45850bf14c56366659ab627d3adbcd7a87ec64e219e7c6dc5cf990dddff5ac59db76520f2b1c7b5b05ea8d069162ca8a5112e44
  languageName: node
  linkType: hard

"jsbn@npm:1.1.0":
  version: 1.1.0
  resolution: "jsbn@npm:1.1.0"
  checksum: 944f924f2bd67ad533b3850eee47603eed0f6ae425fd1ee8c760f477e8c34a05f144c1bd4f5a5dd1963141dc79a2c55f89ccc5ab77d039e7077f3ad196b64965
  languageName: node
  linkType: hard

"lodash.find@npm:4.6.0":
  version: 4.6.0
  resolution: "lodash.find@npm:4.6.0"
  checksum: b737f849a4fe36f5c3664ea636780dda2fde18335021faf80cdfdcb300ed75441da6f55cfd6de119092d8bb2ddbc4433f4a8de4b99c0b9c8640465b0901c717c
  languageName: node
  linkType: hard

"lodash.max@npm:4.0.1":
  version: 4.0.1
  resolution: "lodash.max@npm:4.0.1"
  checksum: f887b68db054edabe3a4f4877a7b7d0bef6e4a29e9371ea4d711a6ed0ca2393659a879158c609649aabd0337528ec58ed5b1e9581fed536ec0b8ae45a6e5f278
  languageName: node
  linkType: hard

"lodash.merge@npm:4.6.2, lodash.merge@npm:^4.6.1, lodash.merge@npm:^4.6.2":
"lodash.padstart@npm:4.6.1":
  version: 4.6.1
  resolution: "lodash.padstart@npm:4.6.1"
  checksum: 0d6ad92c626d351db85de539e41df3238d7d36c5fbfc5f57c4f060c90c73ad9f1db566463487795fdf0bf290a8f133189a0bd91d051032f6eb2d15b7e1863b5e
  languageName: node
  linkType: hard

"lodash.repeat@npm:4.1.0":
  version: 4.1.0
  resolution: "lodash.repeat@npm:4.1.0"
  checksum: dac15fc59ed783678e1a9f986fefa180bfdbf95280852165965ecc8e15b871c6f0eaf7b325768a176014594d5186f1d6558fb72a18527bddd82539fb3ef8a4d3
  languageName: node
  linkType: hard

"sprintf-js@npm:1.1.2, sprintf-js@npm:^1.0.3, sprintf-js@npm:^1.1.2":
Fixed code:     "ipaddr.js": "^2.0.1",
// eslint-disable-next-line max-classes-per-file
const ipaddr = require('ipaddr.js')

// 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'
      if (isDisallowedIP(record.address)) {
    if (ipaddr.isValid(options.host) && isDisallowedIP(options.host)) {
    if (ipaddr.isValid(options.host) && isDisallowedIP(options.host)) {
    ipaddr.js: ^2.0.1
"lodash.merge@npm:^4.6.1, lodash.merge@npm:^4.6.2":
"sprintf-js@npm:^1.0.3, sprintf-js@npm:^1.1.2":