@aikidosec/firewall
Version:
Zen by Aikido is an embedded Web Application Firewall that autonomously protects Node.js apps against common and critical attacks
102 lines (101 loc) • 4.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Undici = void 0;
const dns_1 = require("dns");
const AgentSingleton_1 = require("../agent/AgentSingleton");
const Context_1 = require("../agent/Context");
const getNodeVersion_1 = require("../helpers/getNodeVersion");
const isVersionGreaterOrEqual_1 = require("../helpers/isVersionGreaterOrEqual");
const checkContextForSSRF_1 = require("../vulnerabilities/ssrf/checkContextForSSRF");
const inspectDNSLookupCalls_1 = require("../vulnerabilities/ssrf/inspectDNSLookupCalls");
const wrapDispatch_1 = require("./undici/wrapDispatch");
const wrapExport_1 = require("../agent/hooks/wrapExport");
const getHostnameAndPortFromArgs_1 = require("./undici/getHostnameAndPortFromArgs");
const methods = [
"request",
"stream",
"pipeline",
"connect",
"fetch",
"upgrade",
];
class Undici {
inspectHostname(agent, hostname, port, method) {
// 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: `undici.${method}`,
context,
port,
});
}
inspect(args, agent, method) {
const hostnameAndPort = (0, getHostnameAndPortFromArgs_1.getHostnameAndPortFromArgs)(args);
if (hostnameAndPort) {
const attack = this.inspectHostname(agent, hostnameAndPort.hostname, hostnameAndPort.port, method);
if (attack) {
return attack;
}
}
return undefined;
}
patchGlobalDispatcher(agent, undiciModule) {
const dispatcher = new undiciModule.Agent({
connect: {
lookup: (0, inspectDNSLookupCalls_1.inspectDNSLookupCalls)(dns_1.lookup, agent, "undici",
// We don't know the method here, so we just use "undici.[method]"
"undici.[method]"),
},
});
dispatcher.dispatch = (0, wrapDispatch_1.wrapDispatch)(dispatcher.dispatch, agent);
// We'll set a global dispatcher that will inspect the resolved IP address (and thus preventing TOCTOU attacks)
undiciModule.setGlobalDispatcher(dispatcher);
}
wrap(hooks) {
if (!(0, isVersionGreaterOrEqual_1.isVersionGreaterOrEqual)("16.8.0", (0, getNodeVersion_1.getSemverNodeVersion)())) {
// Undici requires Node.js 16.8+ (due to web streams)
return;
}
hooks
.addPackage("undici")
.withVersion("^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0")
.onRequire((exports, pkgInfo) => {
const agent = (0, AgentSingleton_1.getInstance)();
if (!agent) {
// No agent, we can't do anything
return;
}
// Immediately patch the global dispatcher before returning the module
// The global dispatcher might be overwritten by the user
// But at least they have a reference to our dispatcher instead of the original one
// (In case the user has a custom dispatcher that conditionally calls the original dispatcher)
this.patchGlobalDispatcher(agent, exports);
// Print a warning that we can't provide protection if setGlobalDispatcher is called
(0, wrapExport_1.wrapExport)(exports, "setGlobalDispatcher", pkgInfo, {
kind: undefined,
inspectArgs: (_, agent) => {
agent.log(`undici.setGlobalDispatcher(..) was called, we can't guarantee protection!`);
},
});
// Wrap all methods that can make requests
for (const method of methods) {
(0, wrapExport_1.wrapExport)(exports, method, pkgInfo, {
kind: "outgoing_http_op",
// Whenever a request is made, we'll check the hostname whether it's a private IP
inspectArgs: (args, agent) => {
return this.inspect(args, agent, method);
},
});
}
});
}
}
exports.Undici = Undici;