Просмотр исходного кода

@uppy/companion: allow local ips when testing (#4328)

* allow local ips when testing

in development when `allowLocalUrls` is true

* fix oops

* improve error and add test

* refactor

* add more ip block tests
Mikael Finstad 2 лет назад
Родитель
Сommit
56cbb3e1eb

+ 43 - 39
packages/@uppy/companion/src/server/helpers/request.js

@@ -9,6 +9,7 @@ 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
@@ -16,6 +17,7 @@ const FORBIDDEN_IP_ADDRESS = 'Forbidden IP address'
 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)
@@ -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)
 

+ 40 - 5
packages/@uppy/companion/test/__tests__/http-agent.js

@@ -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', () => {
@@ -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)
@@ -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))
+    }
   })
 })