@aikidosec/firewall
Version:
Zen by Aikido is an embedded Application Firewall that autonomously protects Node.js apps against common and critical attacks, provides rate limiting, detects malicious traffic (including bots), and more.
134 lines (133 loc) • 5.87 kB
JavaScript
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;
;