UNPKG

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