@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.
96 lines (95 loc) • 4.61 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.wrapDispatch = wrapDispatch;
const getMetadataForSSRFAttack_1 = require("../../vulnerabilities/ssrf/getMetadataForSSRFAttack");
const RequestContextStorage_1 = require("./RequestContextStorage");
const Context_1 = require("../../agent/Context");
const tryParseURL_1 = require("../../helpers/tryParseURL");
const getPortFromURL_1 = require("../../helpers/getPortFromURL");
const Attack_1 = require("../../agent/Attack");
const escapeHTML_1 = require("../../helpers/escapeHTML");
const isRedirectToPrivateIP_1 = require("../../vulnerabilities/ssrf/isRedirectToPrivateIP");
const wrapOnHeaders_1 = require("./wrapOnHeaders");
const cleanError_1 = require("../../helpers/cleanError");
const cleanupStackTrace_1 = require("../../helpers/cleanupStackTrace");
const getLibraryRoot_1 = require("../../helpers/getLibraryRoot");
/**
* Wraps the dispatch function of the undici client to store the port of the request in the context.
* This is needed to prevent false positives for SSRF vulnerabilities.
* At a dns request, the port is not known, so we need to store it in the context to prevent the following scenario:
* 1. Userinput includes localhost:4000 in the host header, because the application is running on port 4000
* 2. The application makes a fetch request to localhost:5000 - this would be blocked as SSRF, because the port is not known
*
* We can not store the port in the context directly inside our inspect functions, because the order in which the requests are made is not guaranteed.
* So for example if Promise.all is used, the dns request for one request could be made after the fetch request of another request.
*
*/
function wrapDispatch(orig, agent) {
return function wrap(opts, handler) {
const context = (0, Context_1.getContext)();
if (!context || !opts || !opts.origin || !handler) {
return orig.apply(
// @ts-expect-error We don't know the type of this
this, [opts, handler]);
}
let url;
if (typeof opts.origin === "string" && typeof opts.path === "string") {
url = (0, tryParseURL_1.tryParseURL)(opts.origin + opts.path);
}
else if (opts.origin instanceof URL) {
if (typeof opts.path === "string") {
url = (0, tryParseURL_1.tryParseURL)(opts.origin.href + opts.path);
}
else {
url = opts.origin;
}
}
if (!url) {
return orig.apply(
// @ts-expect-error We don't know the type of this
this, [opts, handler]);
}
blockRedirectToPrivateIP(url, context, agent);
const port = (0, getPortFromURL_1.getPortFromURL)(url);
// Wrap onHeaders to check for redirects
handler.onHeaders = (0, wrapOnHeaders_1.wrapOnHeaders)(handler.onHeaders, { port, url }, context);
return RequestContextStorage_1.RequestContextStorage.run({ port, url }, () => {
return orig.apply(
// @ts-expect-error We don't know the type of this
this, [opts, handler]);
});
};
}
/**
* Checks if it's a redirect to a private IP that originates from a user input and blocks it if it is.
*/
function blockRedirectToPrivateIP(url, context, agent) {
const isBypassedIP = context &&
context.remoteAddress &&
agent.getConfig().isBypassedIP(context.remoteAddress);
if (isBypassedIP) {
// If the IP address is allowed, we don't need to block the request
return;
}
const found = (0, isRedirectToPrivateIP_1.isRedirectToPrivateIP)(url, context);
if (found) {
agent.onDetectedAttack({
module: "undici",
operation: "fetch",
kind: "ssrf",
source: found.source,
blocked: agent.shouldBlock(),
stack: (0, cleanupStackTrace_1.cleanupStackTrace)(new Error().stack, (0, getLibraryRoot_1.getLibraryRoot)()),
paths: found.pathsToPayload,
metadata: (0, getMetadataForSSRFAttack_1.getMetadataForSSRFAttack)({
hostname: found.hostname,
port: found.port,
}),
request: context,
payload: found.payload,
});
if (agent.shouldBlock()) {
throw (0, cleanError_1.cleanError)(new Error(`Zen has blocked ${(0, Attack_1.attackKindHumanName)("ssrf")}: fetch(...) originating from ${found.source}${(0, escapeHTML_1.escapeHTML)((found.pathsToPayload || []).join())}`));
}
}
}
;