@aikidosec/firewall
Version:
Zen by Aikido is an embedded Web Application Firewall that autonomously protects Node.js apps against common and critical attacks
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())}`));
}
}
}