@aikidosec/firewall
Version:
Zen by Aikido is an embedded Web Application Firewall that autonomously protects Node.js apps against common and critical attacks
165 lines (164 loc) • 8.64 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.inspectDNSLookupCalls = inspectDNSLookupCalls;
const net_1 = require("net");
const Attack_1 = require("../../agent/Attack");
const Context_1 = require("../../agent/Context");
const cleanupStackTrace_1 = require("../../helpers/cleanupStackTrace");
const escapeHTML_1 = require("../../helpers/escapeHTML");
const isPlainObject_1 = require("../../helpers/isPlainObject");
const getMetadataForSSRFAttack_1 = require("./getMetadataForSSRFAttack");
const isPrivateIP_1 = require("./isPrivateIP");
const imds_1 = require("./imds");
const RequestContextStorage_1 = require("../../sinks/undici/RequestContextStorage");
const findHostnameInContext_1 = require("./findHostnameInContext");
const getRedirectOrigin_1 = require("./getRedirectOrigin");
const getPortFromURL_1 = require("../../helpers/getPortFromURL");
const getLibraryRoot_1 = require("../../helpers/getLibraryRoot");
const cleanError_1 = require("../../helpers/cleanError");
function inspectDNSLookupCalls(lookup, agent, module, operation, url, stackTraceCallingLocation) {
return function inspectDNSLookup(...args) {
const hostname = args.length > 0 && typeof args[0] === "string" ? args[0] : undefined;
const callback = args.find((arg) => typeof arg === "function");
// If the hostname is an IP address, or if the callback is missing, we don't need to inspect the resolved IPs
if (!hostname || (0, net_1.isIP)(hostname) || !callback) {
return lookup(...args);
}
const options = args.find((arg) => (0, isPlainObject_1.isPlainObject)(arg));
const argsToApply = options
? [
hostname,
options,
wrapDNSLookupCallback(callback, hostname, module, agent, operation, url, stackTraceCallingLocation),
]
: [
hostname,
wrapDNSLookupCallback(callback, hostname, module, agent, operation, url, stackTraceCallingLocation),
];
lookup(...argsToApply);
};
}
// eslint-disable-next-line max-lines-per-function
function wrapDNSLookupCallback(callback, hostname, module, agent, operation, urlArg, callingLocationStackTrace) {
// eslint-disable-next-line max-lines-per-function
return function wrappedDNSLookupCallback(err, addresses, family) {
if (err) {
return callback(err);
}
const context = (0, Context_1.getContext)();
if (context) {
const matches = agent.getConfig().getEndpoints(context);
if (matches.find((endpoint) => endpoint.forceProtectionOff)) {
// User disabled protection for this endpoint, we don't need to inspect the resolved IPs
// Just call the original callback to allow the DNS lookup
return callback(err, addresses, family);
}
}
const resolvedIPAddresses = getResolvedIPAddresses(addresses);
if (resolvesToIMDSIP(resolvedIPAddresses, hostname)) {
// Block stored SSRF attack that target IMDS IP addresses
// An attacker could have stored a hostname in a database that points to an IMDS IP address
// We don't check if the user input contains the hostname because there's no context
if (agent.shouldBlock()) {
return callback(new Error(`Zen has blocked ${(0, Attack_1.attackKindHumanName)("ssrf")}: ${operation}(...) originating from unknown source`));
}
}
if (!context) {
// If there's no context, we can't check if the hostname is in the context
// Just call the original callback to allow the DNS lookup
return callback(err, addresses, family);
}
// This is set if this resolve is part of an outgoing request that we are inspecting
const requestContext = RequestContextStorage_1.RequestContextStorage.getStore();
let port;
if (urlArg) {
port = (0, getPortFromURL_1.getPortFromURL)(urlArg);
}
else if (requestContext) {
port = requestContext.port;
}
const privateIP = resolvedIPAddresses.find(isPrivateIP_1.isPrivateIP);
if (!privateIP) {
// If the hostname doesn't resolve to a private IP address, it's not an SSRF attack
// Just call the original callback to allow the DNS lookup
return callback(err, addresses, family);
}
let found = (0, findHostnameInContext_1.findHostnameInContext)(hostname, context, port);
// The hostname is not found in the context, check if it's a redirect
if (!found && context.outgoingRequestRedirects) {
let url;
// Url arg is passed when wrapping node:http(s), but not for undici / fetch because of the way they are wrapped
// For undici / fetch we need to get the url from the request context, which is an additional async context for outgoing requests,
// not to be confused with the "normal" context used in wide parts of this library
if (urlArg) {
url = urlArg;
}
else if (requestContext) {
url = new URL(requestContext.url);
}
if (url) {
// Get the origin of the redirect chain (the first URL in the chain), if the URL is the result of a redirect
const redirectOrigin = (0, getRedirectOrigin_1.getRedirectOrigin)(context.outgoingRequestRedirects, url);
// If the URL is the result of a redirect, get the origin of the redirect chain for reporting the attack source
if (redirectOrigin) {
found = (0, findHostnameInContext_1.findHostnameInContext)(redirectOrigin.hostname, context, (0, getPortFromURL_1.getPortFromURL)(redirectOrigin));
}
}
}
if (!found) {
// If we can't find the hostname in the context, it's not an SSRF attack
// Just call the original callback to allow the DNS lookup
return callback(err, addresses, family);
}
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
// Just call the original callback to allow the DNS lookup
return callback(err, addresses, family);
}
// Used to get the stack trace of the calling location
// We don't throw the error, we just use it to get the stack trace
const stackTraceError = callingLocationStackTrace || new Error();
agent.onDetectedAttack({
module: module,
operation: operation,
kind: "ssrf",
source: found.source,
blocked: agent.shouldBlock(),
stack: (0, cleanupStackTrace_1.cleanupStackTrace)(stackTraceError.stack, (0, getLibraryRoot_1.getLibraryRoot)()),
paths: found.pathsToPayload,
metadata: (0, getMetadataForSSRFAttack_1.getMetadataForSSRFAttack)({ hostname, port }),
request: context,
payload: found.payload,
});
if (agent.shouldBlock()) {
return callback((0, cleanError_1.cleanError)(new Error(`Zen has blocked ${(0, Attack_1.attackKindHumanName)("ssrf")}: ${operation}(...) originating from ${found.source}${(0, escapeHTML_1.escapeHTML)((found.pathsToPayload || []).join())}`)));
}
// If the attack should not be blocked
// Just call the original callback to allow the DNS lookup
return callback(err, addresses, family);
};
}
function getResolvedIPAddresses(addresses) {
const resolvedIPAddresses = [];
for (const address of Array.isArray(addresses) ? addresses : [addresses]) {
if (typeof address === "string") {
resolvedIPAddresses.push(address);
continue;
}
if ((0, isPlainObject_1.isPlainObject)(address) && address.address) {
resolvedIPAddresses.push(address.address);
}
}
return resolvedIPAddresses;
}
function resolvesToIMDSIP(resolvedIPAddresses, hostname) {
// Allow access to Google Cloud metadata service as you need to set specific headers to access it
// We don't want to block legitimate requests
if ((0, imds_1.isTrustedHostname)(hostname)) {
return false;
}
return resolvedIPAddresses.some((ip) => (0, imds_1.isIMDSIPAddress)(ip));
}