|
@@ -1,7 +1,6 @@
|
|
|
// eslint-disable-next-line max-classes-per-file
|
|
|
const http = require('node:http')
|
|
|
const https = require('node:https')
|
|
|
-const { URL } = require('node:url')
|
|
|
const dns = require('node:dns')
|
|
|
const ipaddr = require('ipaddr.js')
|
|
|
const got = require('got').default
|
|
@@ -9,10 +8,7 @@ const path = require('node:path')
|
|
|
const contentDisposition = require('content-disposition')
|
|
|
const validator = require('validator')
|
|
|
|
|
|
-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
|
|
@@ -20,31 +16,6 @@ const FORBIDDEN_RESOLVED_IP_ADDRESS = 'Forbidden resolved 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)
|
|
|
-
|
|
|
- return ({ headers }) => {
|
|
|
- if (!isEnabled) return true
|
|
|
-
|
|
|
- let redirectURL = null
|
|
|
- try {
|
|
|
- redirectURL = new URL(headers.location, requestURL)
|
|
|
- } catch (err) {
|
|
|
- return false
|
|
|
- }
|
|
|
-
|
|
|
- const shouldRedirect = redirectURL.protocol === requestURL.protocol
|
|
|
- if (!shouldRedirect) {
|
|
|
- logger.info(
|
|
|
- `blocking redirect from ${requestURL} to ${redirectURL}`, 'redirect.protection',
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
- return shouldRedirect
|
|
|
- }
|
|
|
-}
|
|
|
|
|
|
/**
|
|
|
* Validates that the download URL is secure
|
|
@@ -83,14 +54,19 @@ const getProtectedHttpAgent = ({ protocol, blockLocalIPs }) => {
|
|
|
}
|
|
|
|
|
|
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
|
|
|
- }
|
|
|
+ // because dns.lookup seems to be called with option `all: true`, if we are on an ipv6 system,
|
|
|
+ // `addresses` could contain a list of ipv4 addresses as well as ipv6 mapped addresses (rfc6052) which we cannot allow
|
|
|
+ // however we should still allow any valid ipv4 addresses, so we filter out the invalid addresses
|
|
|
+ const validAddresses = !blockLocalIPs ? toValidate : toValidate.filter(({ address }) => !isDisallowedIP(address))
|
|
|
+
|
|
|
+ // and check if there's anything left after we filtered:
|
|
|
+ if (validAddresses.length === 0) {
|
|
|
+ callback(new Error(`Forbidden resolved IP address ${hostname} -> ${toValidate.map(({ address }) => address).join(', ')}`), addresses, maybeFamily)
|
|
|
+ return
|
|
|
}
|
|
|
|
|
|
- callback(err, addresses, maybeFamily)
|
|
|
+ const ret = Array.isArray(addresses) ? validAddresses : validAddresses[0].address;
|
|
|
+ callback(err, ret, maybeFamily)
|
|
|
})
|
|
|
}
|
|
|
|
|
@@ -108,23 +84,15 @@ const getProtectedHttpAgent = ({ protocol, blockLocalIPs }) => {
|
|
|
|
|
|
module.exports.getProtectedHttpAgent = getProtectedHttpAgent
|
|
|
|
|
|
-function getProtectedGot ({ url, blockLocalIPs }) {
|
|
|
+function getProtectedGot ({ blockLocalIPs }) {
|
|
|
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)
|
|
|
-
|
|
|
- const beforeRedirect = (options, response) => {
|
|
|
- const allowRedirect = redirectEvaluator(response)
|
|
|
- if (!allowRedirect) {
|
|
|
- throw new Error(`Redirect evaluator does not allow the redirect to ${response.headers.location}`)
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
// @ts-ignore
|
|
|
- return got.extend({ hooks: { beforeRedirect: [beforeRedirect] }, agent: { http: httpAgent, https: httpsAgent } })
|
|
|
+ return got.extend({ agent: { http: httpAgent, https: httpsAgent } })
|
|
|
}
|
|
|
|
|
|
module.exports.getProtectedGot = getProtectedGot
|
|
@@ -138,7 +106,7 @@ module.exports.getProtectedGot = getProtectedGot
|
|
|
*/
|
|
|
exports.getURLMeta = async (url, blockLocalIPs = false) => {
|
|
|
async function requestWithMethod (method) {
|
|
|
- const protectedGot = getProtectedGot({ url, blockLocalIPs })
|
|
|
+ const protectedGot = getProtectedGot({ blockLocalIPs })
|
|
|
const stream = protectedGot.stream(url, { method, throwHttpErrors: false })
|
|
|
|
|
|
return new Promise((resolve, reject) => (
|