UNPKG

@aikidosec/firewall

Version:

Zen by Aikido is an embedded Web Application Firewall that autonomously protects Node.js apps against common and critical attacks

134 lines (133 loc) 5.87 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Fetch = void 0; /* eslint-disable max-lines-per-function */ const dns_1 = require("dns"); const Context_1 = require("../agent/Context"); const getPortFromURL_1 = require("../helpers/getPortFromURL"); const tryParseURL_1 = require("../helpers/tryParseURL"); const checkContextForSSRF_1 = require("../vulnerabilities/ssrf/checkContextForSSRF"); const inspectDNSLookupCalls_1 = require("../vulnerabilities/ssrf/inspectDNSLookupCalls"); const wrapDispatch_1 = require("./undici/wrapDispatch"); class Fetch { constructor() { this.patchedGlobalDispatcher = false; } inspectHostname(agent, hostname, port) { // Let the agent know that we are connecting to this hostname // This is to build a list of all hostnames that the application is connecting to if (typeof port === "number" && port > 0) { agent.onConnectHostname(hostname, port); } const context = (0, Context_1.getContext)(); if (!context) { return undefined; } return (0, checkContextForSSRF_1.checkContextForSSRF)({ hostname: hostname, operation: "fetch", context: context, port: port, }); } inspectFetch(args, agent) { if (args.length > 0) { // URL string if (typeof args[0] === "string" && args[0].length > 0) { const url = (0, tryParseURL_1.tryParseURL)(args[0]); if (url) { const attack = this.inspectHostname(agent, url.hostname, (0, getPortFromURL_1.getPortFromURL)(url)); if (attack) { return attack; } } } // Fetch accepts any object with a stringifier. User input may be an array if the user provides an array // query parameter (e.g., ?example[0]=https://example.com/) in frameworks like Express. Since an Array has // a default stringifier, this is exploitable in a default setup. // The following condition ensures that we see the same value as what's passed down to the sink. if (Array.isArray(args[0])) { const url = (0, tryParseURL_1.tryParseURL)(args[0].toString()); if (url) { const attack = this.inspectHostname(agent, url.hostname, (0, getPortFromURL_1.getPortFromURL)(url)); if (attack) { return attack; } } } // URL object if (args[0] instanceof URL && args[0].hostname.length > 0) { const attack = this.inspectHostname(agent, args[0].hostname, (0, getPortFromURL_1.getPortFromURL)(args[0])); if (attack) { return attack; } } // Request object if (args[0] instanceof Request) { const url = (0, tryParseURL_1.tryParseURL)(args[0].url); if (url) { const attack = this.inspectHostname(agent, url.hostname, (0, getPortFromURL_1.getPortFromURL)(url)); if (attack) { return attack; } } } } return undefined; } // We'll set a global dispatcher that will allow us to inspect the resolved IPs (and thus preventing TOCTOU attacks) patchGlobalDispatcher(agent) { const undiciGlobalDispatcherSymbol = Symbol.for("undici.globalDispatcher.1"); // @ts-expect-error Type is not defined const dispatcher = globalThis[undiciGlobalDispatcherSymbol]; if (!dispatcher) { agent.log(`global dispatcher not found for fetch, we can't provide protection!`); return; } if (dispatcher.constructor.name !== "Agent") { agent.log(`Expected Agent as global dispatcher for fetch but found ${dispatcher.constructor.name}, we can't provide protection!`); return; } try { // @ts-expect-error Type is not defined globalThis[undiciGlobalDispatcherSymbol] = new dispatcher.constructor({ connect: { lookup: (0, inspectDNSLookupCalls_1.inspectDNSLookupCalls)(dns_1.lookup, agent, "fetch", "fetch"), }, }); // @ts-expect-error Type is not defined globalThis[undiciGlobalDispatcherSymbol].dispatch = (0, wrapDispatch_1.wrapDispatch)( // @ts-expect-error Type is not defined globalThis[undiciGlobalDispatcherSymbol].dispatch, agent); } catch { agent.log(`Failed to patch global dispatcher for fetch, we can't provide protection!`); } } wrap(hooks) { if (typeof globalThis.fetch === "function") { // Fetch is lazy loaded in Node.js // By calling fetch() we ensure that the global dispatcher is available try { // @ts-expect-error Type is not defined globalThis.fetch().catch(() => { }); } catch { // Ignore errors } } hooks.addGlobal("fetch", { kind: "outgoing_http_op", // Whenever a request is made, we'll check the hostname whether it's a private IP inspectArgs: (args, agent) => this.inspectFetch(args, agent), modifyArgs: (args, agent) => { if (!this.patchedGlobalDispatcher) { this.patchGlobalDispatcher(agent); this.patchedGlobalDispatcher = true; } return args; }, }); } } exports.Fetch = Fetch;