UNPKG

@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
"use strict"; 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())}`)); } } }