@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.
165 lines (164 loc) • 8.65 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, privateIP }),
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));
}
;