فهرست منبع

companion: validate URL ip address via dns lookup

ifedapoolarewaju 5 سال پیش
والد
کامیت
4aeef4dac0

+ 11 - 6
packages/@uppy/companion/src/server/controllers/url.js

@@ -3,6 +3,7 @@ const request = require('request')
 const Uploader = require('../Uploader')
 const validator = require('validator')
 const utils = require('../helpers/utils')
+const { getProtectedHttpAgent } = require('../helpers/request')
 const logger = require('../logger')
 
 module.exports = () => {
@@ -19,12 +20,13 @@ module.exports = () => {
  */
 const meta = (req, res) => {
   logger.debug('URL file import handler running', null, req.id)
-  if (!validateURL(req.body.url, req.companion.options.debug)) {
+  const debug = req.companion.options.debug
+  if (!validateURL(req.body.url, debug)) {
     logger.debug('Invalid request body detected. Exiting url meta handler.', null, req.id)
     return res.status(400).json({ error: 'Invalid request body' })
   }
 
-  utils.getURLMeta(req.body.url)
+  utils.getURLMeta(req.body.url, !debug)
     .then((meta) => res.json(meta))
     .catch((err) => {
       logger.error(err, 'controller.url.meta.error', req.id)
@@ -41,7 +43,8 @@ const meta = (req, res) => {
  */
 const get = (req, res) => {
   logger.debug('URL file import handler running', null, req.id)
-  if (!validateURL(req.body.url, req.companion.options.debug)) {
+  const debug = req.companion.options.debug
+  if (!validateURL(req.body.url, debug)) {
     logger.debug('Invalid request body detected. Exiting url import handler.', null, req.id)
     return res.status(400).json({ error: 'Invalid request body' })
   }
@@ -61,7 +64,7 @@ const get = (req, res) => {
       logger.debug('Waiting for socket connection before beginning remote download.', null, req.id)
       uploader.onSocketReady(() => {
         logger.debug('Socket connection received. Starting remote download.', null, req.id)
-        downloadURL(req.body.url, uploader.handleChunk.bind(uploader), req.id)
+        downloadURL(req.body.url, uploader.handleChunk.bind(uploader), !debug, req.id)
       })
 
       const response = uploader.getResponse()
@@ -102,13 +105,15 @@ const validateURL = (url, debug) => {
  *
  * @param {string} url
  * @param {typeof Function} onDataChunk
+ * @param {boolean} blockLocalIPs
  * @param {string=} traceId
  */
-const downloadURL = (url, onDataChunk, traceId) => {
+const downloadURL = (url, onDataChunk, blockLocalIPs, traceId) => {
   const opts = {
     uri: url,
     method: 'GET',
-    followAllRedirects: false
+    followAllRedirects: false,
+    agentClass: getProtectedHttpAgent(utils.parseURL(url).protocol, blockLocalIPs)
   }
 
   request(opts)

+ 114 - 0
packages/@uppy/companion/src/server/helpers/request.js

@@ -0,0 +1,114 @@
+const http = require('http')
+const https = require('https')
+const dns = require('dns')
+
+/**
+ * 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}
+ */
+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
+}
+
+/**
+ * Returns http Agent that will prevent requests to private IPs (to preven SSRF)
+ * @param {string} protocol http or https protocol needed for the request
+ * @param {boolean} blockPrivateIPs if set to false, this protection will be disabled
+ */
+module.exports.getProtectedHttpAgent = (protocol, blockPrivateIPs) => {
+  if (blockPrivateIPs) {
+    return protocol === 'https' ? HttpsAgent : HttpAgent
+  }
+
+  return protocol === 'https' ? https.Agent : http.Agent
+}
+
+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 (isPrivateIP(record.address)) {
+        callback(new Error('forbidden ip address'), addresses, maybeFamily)
+        return
+      }
+    }
+
+    callback(err, addresses, maybeFamily)
+  })
+}
+
+class HttpAgent extends http.Agent {
+  createConnection (options, callback) {
+    options.lookup = dnsLookup
+    // @ts-ignore
+    return super.createConnection(options, callback)
+  }
+}
+
+class HttpsAgent extends https.Agent {
+  createConnection (options, callback) {
+    options.lookup = dnsLookup
+    // @ts-ignore
+    return super.createConnection(options, callback)
+  }
+}

+ 5 - 2
packages/@uppy/companion/src/server/helpers/utils.js

@@ -1,6 +1,7 @@
 const request = require('request')
 const urlParser = require('url')
 const crypto = require('crypto')
+const { getProtectedHttpAgent } = require('./request')
 
 /**
  *
@@ -57,14 +58,16 @@ exports.parseURL = (url) => {
  * Gets the size and content type of a url's content
  *
  * @param {string} url
+ * @param {boolean=} blockLocalIPs
  * @return {Promise}
  */
-exports.getURLMeta = (url) => {
+exports.getURLMeta = (url, blockLocalIPs = false) => {
   return new Promise((resolve, reject) => {
     const opts = {
       uri: url,
       method: 'HEAD',
-      followAllRedirects: true
+      followAllRedirects: false,
+      agentClass: getProtectedHttpAgent(exports.parseURL(url).protocol, blockLocalIPs)
     }
 
     request(opts, (err, response, body) => {