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