CVE-2022-0086 (published 2022-01-04T18:15Z)

uppy is vulnerable to Server-Side Request Forgery (SSRF)

Severity: HIGH
CVSS v2 base score: 7.5 (AV:N/AC:L/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-918
cwe_name: Server-Side Request Forgery (SSRF)
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.
extended_description: By providing URLs to unexpected hosts or ports, attackers can make it appear that the server is sending the request, possibly bypassing access controls such as firewalls that prevent the attackers from accessing the URLs directly. The server can be used as a proxy to conduct port scanning of hosts in internal networks, use other URLs such as that can access documents on the system (using file://), or use other protocols such as gopher:// or tftp://, which may provide greater control over the contents of requests.
         

Fix information:
repo_url: https://github.com/transloadit/uppy
commit_details: repo_url: https://github.com/transloadit/uppy
author: Mikael Finstad
author_date: 2022-01-04 17:02:44+07:00
msg: improve private ip check (#3403)

https://huntr.dev/bounties/c1c03ef6-3f18-4976-a9ad-08c251279122/
repository: repo_url: https://github.com/transloadit/uppy
repo_name: transloadit/uppy
description: The next open source file uploader for web browsers :dog: 
repo_language: TypeScript
owner: transloadit
file_changes: filename: package.json
change_type: ModificationType.MODIFY
diff: @@ -45,7 +45,7 @@
     "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",

programming_language: JavaScript
filename: request.js
change_type: ModificationType.MODIFY
diff: @@ -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
     }

programming_language: JavaScript
filename: yarn.lock
change_type: ModificationType.MODIFY
diff: @@ -8100,7 +8100,7 @@ __metadata:
     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
@@ -23125,21 +23125,6 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
   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"
@@ -25047,13 +25032,6 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
   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"
@@ -26431,13 +26409,6 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
   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"
@@ -26543,13 +26514,6 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
   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"
@@ -26564,7 +26528,7 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
   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
@@ -26585,13 +26549,6 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
   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"
@@ -26606,13 +26563,6 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
   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"
@@ -37701,7 +37651,7 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
   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

programming_language: HTML