@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.
79 lines (78 loc) • 3.82 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.wrapResponseHandler = wrapResponseHandler;
const Context_1 = require("../../agent/Context");
const getNodeVersion_1 = require("../../helpers/getNodeVersion");
const getPortFromURL_1 = require("../../helpers/getPortFromURL");
const isRedirectStatusCode_1 = require("../../helpers/isRedirectStatusCode");
const tryParseURL_1 = require("../../helpers/tryParseURL");
const findHostnameInContext_1 = require("../../vulnerabilities/ssrf/findHostnameInContext");
const getRedirectOrigin_1 = require("../../vulnerabilities/ssrf/getRedirectOrigin");
const getUrlFromHTTPRequestArgs_1 = require("./getUrlFromHTTPRequestArgs");
/**
* We are wrapping the response handler for outgoing HTTP requests to detect redirects.
* If the response is a redirect, we will add the redirect to the context to be able to detect SSRF attacks with redirects.
*/
function wrapResponseHandler(args, module, fn) {
return function responseHandler(res) {
// If you don't have a response handler, pre node 19, the process will exit
// From node 19 onwards, the process will not exit if there is no response handler
if ((0, getNodeVersion_1.getMajorNodeVersion)() >= 19) {
// Need to attach data & end event handler otherwise the process will not exit
// As safety we'll attach the handlers only if there are no listeners
if (res.rawListeners("data").length === 0) {
res.on("data", () => { });
}
if (res.rawListeners("end").length === 0) {
res.on("end", () => { });
}
}
const context = (0, Context_1.getContext)();
if (context) {
onHTTPResponse(args, module, res, context);
}
// eslint-disable-next-line prefer-rest-params
fn(...arguments);
};
}
function onHTTPResponse(args, module, res, context) {
if (!res.statusCode || !(0, isRedirectStatusCode_1.isRedirectStatusCode)(res.statusCode)) {
return;
}
if (typeof res.headers.location !== "string") {
return;
}
const destination = (0, tryParseURL_1.tryParseURL)(res.headers.location);
if (!destination) {
return;
}
const source = (0, getUrlFromHTTPRequestArgs_1.getUrlFromHTTPRequestArgs)(args, module);
if (!source) {
return;
}
addRedirectToContext(source, destination, context);
}
/**
* Adds redirects with user provided hostname / url to the context to prevent SSRF attacks with redirects.
*/
function addRedirectToContext(source, destination, context) {
let redirectOrigin;
const sourcePort = (0, getPortFromURL_1.getPortFromURL)(source);
// Check if the source hostname is in the context - is true if it's the first redirect in the chain and the user input is the source
const found = (0, findHostnameInContext_1.findHostnameInContext)(source.hostname, context, sourcePort);
// If the source hostname is not in the context, check if it's a redirect in a already existing chain
if (!found && context.outgoingRequestRedirects) {
// Get initial source of the redirect chain (first redirect), if url is part of a redirect chain
redirectOrigin = (0, getRedirectOrigin_1.getRedirectOrigin)(context.outgoingRequestRedirects, source);
}
// Get existing redirects or create a new array
const outgoingRedirects = context.outgoingRequestRedirects || [];
// If it's 1. a initial redirect with user provided url or 2. a redirect in an existing chain, add it to the context
if (found || redirectOrigin) {
outgoingRedirects.push({
source,
destination,
});
(0, Context_1.updateContext)(context, "outgoingRequestRedirects", outgoingRedirects);
}
}
;