diff --git a/docs/ssrf.md b/docs/ssrf.md index 024546ba1..a5d7dfef6 100644 --- a/docs/ssrf.md +++ b/docs/ssrf.md @@ -25,7 +25,15 @@ We don't protect against stored SSRF attacks, where an attacker injects a malici ## Which built-in modules are protected? Firewall protects against SSRF attacks in the following built-in modules: + * `http` * `https` * `undici` * `globalThis.fetch` (Node.js 18+) + +Modules that use `node:http` or `node:https` are also supported: + +* `axios` +* `node-fetch` +* `follow-redirects` +* ... diff --git a/library/sinks/Fetch.ts b/library/sinks/Fetch.ts index 730bb1ce7..aea43a965 100644 --- a/library/sinks/Fetch.ts +++ b/library/sinks/Fetch.ts @@ -119,7 +119,8 @@ export class Fetch implements Wrapper { globalThis[undiciGlobalDispatcherSymbol].dispatch = wrapDispatch( // @ts-expect-error Type is not defined globalThis[undiciGlobalDispatcherSymbol].dispatch, - agent + agent, + true ); } catch (error) { agent.log( diff --git a/library/sinks/Undici.tests.ts b/library/sinks/Undici.tests.ts index 1685a28a8..f4580f2a0 100644 --- a/library/sinks/Undici.tests.ts +++ b/library/sinks/Undici.tests.ts @@ -10,6 +10,17 @@ import { wrap } from "../helpers/wrap"; import { getMajorNodeVersion } from "../helpers/getNodeVersion"; import { Undici } from "./Undici"; +const redirectTestUrl = "http://ssrf-redirects.testssandbox.com"; +const redirectTestUrl2 = + "http://firewallssrfredirects-env-2.eba-7ifve22q.eu-north-1.elasticbeanstalk.com"; + +const redirectUrl = { + ip: `${redirectTestUrl}/ssrf-test`, // Redirects to http://127.0.0.1/test + domain: `${redirectTestUrl}/ssrf-test-domain`, // Redirects to http://local.aikido.io/test + ipTwice: `${redirectTestUrl}/ssrf-test-twice`, // Redirects to /ssrf-test + domainTwice: `${redirectTestUrl}/ssrf-test-domain-twice`, // Redirects to /ssrf-test-domain +}; + export function createUndiciTests(undiciPkgName: string, port: number) { const calls: Record = {}; wrap(dns, "lookup", function lookup(original) { @@ -349,6 +360,113 @@ export function createUndiciTests(undiciPkgName: string, port: number) { } ); + await runWithContext( + { + ...createContext(), + body: { image: redirectUrl.ip }, + }, + async () => { + const error = await t.rejects( + async () => + await request(redirectUrl.ip, { + maxRedirections: 1, + }) + ); + if (error instanceof Error) { + t.same( + error.message, + "Zen has blocked a server-side request forgery: undici.[method](...) originating from body.image" + ); + } + } + ); + + await runWithContext( + { + ...createContext(), + body: { image: redirectUrl.ipTwice }, + }, + async () => { + const error = await t.rejects( + async () => + await request(redirectUrl.ipTwice, { + maxRedirections: 2, + }) + ); + if (error instanceof Error) { + t.same( + error.message, + "Zen has blocked a server-side request forgery: undici.[method](...) originating from body.image" + ); + } + } + ); + + await runWithContext( + { + ...createContext(), + body: { image: redirectUrl.domain }, + }, + async () => { + const error = await t.rejects( + async () => + await request(redirectUrl.domain, { + maxRedirections: 2, + }) + ); + if (error instanceof Error) { + t.same( + error.message, + "Zen has blocked a server-side request forgery: undici.[method](...) originating from body.image" + ); + } + } + ); + + await runWithContext( + { + ...createContext(), + body: { image: redirectUrl.domainTwice }, + }, + async () => { + const error = await t.rejects( + async () => + await request(redirectUrl.domainTwice, { + maxRedirections: 2, + }) + ); + if (error instanceof Error) { + t.same( + error.message, + "Zen has blocked a server-side request forgery: undici.[method](...) originating from body.image" + ); + } + } + ); + + await runWithContext( + { + ...createContext(), + body: { + image: `${redirectTestUrl2}/ssrf-test-absolute-domain`, + }, + }, + async () => { + const error = await t.rejects( + async () => + await request(`${redirectTestUrl2}/ssrf-test-absolute-domain`, { + maxRedirections: 2, + }) + ); + if (error instanceof Error) { + t.same( + error.message, + "Zen has blocked a server-side request forgery: undici.[method](...) originating from body.image" + ); + } + } + ); + logger.clear(); setGlobalDispatcher(new UndiciAgent({})); t.same(logger.getMessages(), [ diff --git a/library/sinks/Undici.ts b/library/sinks/Undici.ts index 041dcca52..f4385f197 100644 --- a/library/sinks/Undici.ts +++ b/library/sinks/Undici.ts @@ -1,7 +1,7 @@ import { lookup } from "dns"; import { Agent } from "../agent/Agent"; import { getInstance } from "../agent/AgentSingleton"; -import { getContext } from "../agent/Context"; +import { bindContext, getContext } from "../agent/Context"; import { Hooks } from "../agent/hooks/Hooks"; import { InterceptorResult } from "../agent/hooks/InterceptorResult"; import { Wrapper } from "../agent/Wrapper"; @@ -11,7 +11,9 @@ import { checkContextForSSRF } from "../vulnerabilities/ssrf/checkContextForSSRF import { inspectDNSLookupCalls } from "../vulnerabilities/ssrf/inspectDNSLookupCalls"; import { wrapDispatch } from "./undici/wrapDispatch"; import { wrapExport } from "../agent/hooks/wrapExport"; +import { wrapNewInstance } from "../agent/hooks/wrapNewInstance"; import { getHostnameAndPortFromArgs } from "./undici/getHostnameAndPortFromArgs"; +import { wrapOnHeaders } from "./undici/wrapOnHeaders"; const methods = [ "request", @@ -84,12 +86,33 @@ export class Undici implements Wrapper { }, }); - dispatcher.dispatch = wrapDispatch(dispatcher.dispatch, agent); + dispatcher.dispatch = wrapDispatch(dispatcher.dispatch, agent, false); // We'll set a global dispatcher that will inspect the resolved IP address (and thus preventing TOCTOU attacks) undiciModule.setGlobalDispatcher(dispatcher); } + // Wrap the dispatch method of the redirect handler to block http calls to private IPs if it's a redirect + private patchRedirectHandler(instance: unknown) { + const agent = getInstance(); + const context = getContext(); + + if (!agent || !context) { + return instance; + } + + // @ts-expect-error No types for this + instance.dispatch = bindContext( + // @ts-expect-error No types for this + wrapDispatch(instance.dispatch, agent, false, context) + ); + + // @ts-expect-error No types for this + instance.onHeaders = wrapOnHeaders(instance.onHeaders, context); + + return instance; + } + wrap(hooks: Hooks) { if (!isVersionGreaterOrEqual("16.8.0", getSemverNodeVersion())) { // Undici requires Node.js 16.8+ (due to web streams) @@ -132,6 +155,12 @@ export class Undici implements Wrapper { }, }); } + }) + // Todo only working for undici v6 right now + .onFileRequire("lib/handler/redirect-handler.js", (exports, pkgInfo) => { + return wrapNewInstance(exports, undefined, pkgInfo, (instance) => { + return this.patchRedirectHandler(instance); + }); }); } } diff --git a/library/sinks/http-request/onHTTPResponse.ts b/library/sinks/http-request/onHTTPResponse.ts index b661fffa5..43e127b47 100644 --- a/library/sinks/http-request/onHTTPResponse.ts +++ b/library/sinks/http-request/onHTTPResponse.ts @@ -1,10 +1,8 @@ import { IncomingMessage } from "http"; -import { Context, getContext, updateContext } from "../../agent/Context"; -import { getPortFromURL } from "../../helpers/getPortFromURL"; +import { Context } from "../../agent/Context"; import { isRedirectStatusCode } from "../../helpers/isRedirectStatusCode"; import { tryParseURL } from "../../helpers/tryParseURL"; -import { findHostnameInContext } from "../../vulnerabilities/ssrf/findHostnameInContext"; -import { getRedirectOrigin } from "../../vulnerabilities/ssrf/getRedirectOrigin"; +import { addRedirectToContext } from "../../vulnerabilities/ssrf/addRedirectToContext"; export function onHTTPResponse( source: URL, @@ -26,49 +24,3 @@ export function onHTTPResponse( addRedirectToContext(source, destination, context); } - -/** - * Adds redirects with user provided hostname / url to the context to prevent SSRF attacks with redirects. - */ -function addRedirectToContext(source: URL, destination: URL, context: Context) { - let redirectOrigin: URL | undefined; - - const sourcePort = 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 = findHostnameInContext(source.hostname, context, sourcePort); - - // If the source hostname is not in the context, check if it's a redirect in an 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 = getRedirectOrigin( - context.outgoingRequestRedirects, - source - ); - } - - // If it's 1. an initial redirect with user provided url or 2. a redirect in an existing chain, add it to the context - if (found || redirectOrigin) { - addRedirectToChain(source, destination, context); - } -} - -function addRedirectToChain(source: URL, destination: URL, context: Context) { - const outgoingRedirects = context.outgoingRequestRedirects || []; - const alreadyAdded = outgoingRedirects.find( - (r) => - r.source.toString() === source.toString() && - r.destination.toString() === destination.toString() - ); - - if (alreadyAdded) { - return; - } - - outgoingRedirects.push({ - source, - destination, - }); - - updateContext(context, "outgoingRequestRedirects", outgoingRedirects); -} diff --git a/library/sinks/undici/RequestContextStorage.ts b/library/sinks/undici/RequestContextStorage.ts index 3685d255f..d6b398752 100644 --- a/library/sinks/undici/RequestContextStorage.ts +++ b/library/sinks/undici/RequestContextStorage.ts @@ -1,11 +1,47 @@ import { AsyncLocalStorage } from "async_hooks"; +import { Context } from "../../agent/Context"; + +export type UndiciRequestContext = { + url: URL; + port?: number; + isFetch?: boolean; // True if the request is a fetch request, false if it's an direct undici request + inContext?: Context; // Incoming request context +}; /** * This storage is used to store the port of outgoing fetch / undici requests. * This is used to check if ports match when we are inspecting the result of dns resolution. * If the port does not match, it would be a false positive ssrf detection. + * + * Its also used to store the incoming context of a request, if the request is a redirect. */ -export const RequestContextStorage = new AsyncLocalStorage<{ - url: URL; - port?: number; -}>(); +const RequestContextStorage = new AsyncLocalStorage(); + +/** + * Run a function with the given undici request context. + * If the context is already set, the context is updated with the new values. + */ +export function runWithUndiciRequestContext( + context: UndiciRequestContext, + fn: () => T +): T { + const current = RequestContextStorage.getStore(); + + if (current) { + current.url = context.url; + current.port = context.port; + current.isFetch = context.isFetch; + current.inContext = context.inContext; + + return fn(); + } + + return RequestContextStorage.run(context, fn); +} + +/** + * Get the current undici request context (outgoing request). + */ +export function getUndiciRequestContext(): UndiciRequestContext | undefined { + return RequestContextStorage.getStore(); +} diff --git a/library/sinks/undici/getUrlFromOptions.test.ts b/library/sinks/undici/getUrlFromOptions.test.ts new file mode 100644 index 000000000..115f9eddc --- /dev/null +++ b/library/sinks/undici/getUrlFromOptions.test.ts @@ -0,0 +1,27 @@ +import * as t from "tap"; +import { getUrlFromOptions } from "./getUrlFromOptions"; + +t.test("it works", async () => { + t.same( + getUrlFromOptions({ + origin: "http://localhost:3000/", + path: "test?query=1", + }).href, + "http://localhost:3000/test?query=1" + ); + + t.same( + getUrlFromOptions({ + origin: new URL("http://localhost:3000"), + path: "test?query=1", + }).href, + "http://localhost:3000/test?query=1" + ); + + t.same( + getUrlFromOptions({ + origin: new URL("http://localhost:3000"), + }).href, + "http://localhost:3000/" + ); +}); diff --git a/library/sinks/undici/getUrlFromOptions.ts b/library/sinks/undici/getUrlFromOptions.ts new file mode 100644 index 000000000..875ea27b5 --- /dev/null +++ b/library/sinks/undici/getUrlFromOptions.ts @@ -0,0 +1,16 @@ +import { tryParseURL } from "../../helpers/tryParseURL"; + +/** + * Get the URL from the options object of a Undici request. + */ +export function getUrlFromOptions(opts: any): URL | undefined { + if (typeof opts.origin === "string" && typeof opts.path === "string") { + return tryParseURL(opts.origin + opts.path); + } else if (opts.origin instanceof URL) { + if (typeof opts.path === "string") { + return tryParseURL(opts.origin.href + opts.path); + } else { + return opts.origin; + } + } +} diff --git a/library/sinks/undici/onRedirect.ts b/library/sinks/undici/onRedirect.ts deleted file mode 100644 index b804d0f36..000000000 --- a/library/sinks/undici/onRedirect.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Context, updateContext } from "../../agent/Context"; -import { findHostnameInContext } from "../../vulnerabilities/ssrf/findHostnameInContext"; -import { getRedirectOrigin } from "../../vulnerabilities/ssrf/getRedirectOrigin"; -import { RequestContextStorage } from "./RequestContextStorage"; - -/** - * Is called by wrapOnHeaders if a request results in a redirect. - * Check for redirects and store them in the context, if they are originating from user input. - */ -export function onRedirect( - destination: URL, - requestContext: ReturnType, - context: Context -) { - if (!requestContext) { - return; - } - - let redirectOrigin: URL | undefined; - - // 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 = findHostnameInContext( - requestContext.url.hostname, - context, - requestContext.port - ); - - // If the source hostname is not in the context, check if it's a redirect in a already existing chain - if (!found && context.outgoingRequestRedirects) { - redirectOrigin = getRedirectOrigin( - context.outgoingRequestRedirects, - requestContext.url - ); - } - - // 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: requestContext.url, - destination, - }); - - updateContext(context, "outgoingRequestRedirects", outgoingRedirects); - } -} diff --git a/library/sinks/undici/wrapDispatch.ts b/library/sinks/undici/wrapDispatch.ts index fc6f9636e..be65ba6bd 100644 --- a/library/sinks/undici/wrapDispatch.ts +++ b/library/sinks/undici/wrapDispatch.ts @@ -1,13 +1,17 @@ +/* eslint-disable max-lines-per-function */ import type { Dispatcher } from "undici-v6"; +import { + getUndiciRequestContext, + runWithUndiciRequestContext, +} from "./RequestContextStorage"; import { getMetadataForSSRFAttack } from "../../vulnerabilities/ssrf/getMetadataForSSRFAttack"; -import { RequestContextStorage } from "./RequestContextStorage"; import { Context, getContext } from "../../agent/Context"; -import { tryParseURL } from "../../helpers/tryParseURL"; import { getPortFromURL } from "../../helpers/getPortFromURL"; import { Agent } from "../../agent/Agent"; import { attackKindHumanName } from "../../agent/Attack"; import { escapeHTML } from "../../helpers/escapeHTML"; import { isRedirectToPrivateIP } from "../../vulnerabilities/ssrf/isRedirectToPrivateIP"; +import { getUrlFromOptions } from "./getUrlFromOptions"; import { wrapOnHeaders } from "./wrapOnHeaders"; type Dispatch = Dispatcher["dispatch"]; @@ -23,9 +27,22 @@ type Dispatch = Dispatcher["dispatch"]; * So for example if Promise.all is used, the dns request for one request could be made after the fetch request of another request. * */ -export function wrapDispatch(orig: Dispatch, agent: Agent): Dispatch { +export function wrapDispatch( + orig: Dispatch, + agent: Agent, + isFetch: boolean, + contextArg?: Context // Only set if its a nth dispatch after a redirect +): Dispatch { return function wrap(opts, handler) { - const context = getContext(); + let context: Context | undefined; + + // Prefer passed context over the context from the async local storage + // Context is passed as arg if its a nth dispatch after a redirect + if (contextArg) { + context = contextArg; + } else { + context = getContext(); + } if (!context || !opts || !opts.origin || !handler) { return orig.apply( @@ -35,16 +52,7 @@ export function wrapDispatch(orig: Dispatch, agent: Agent): Dispatch { ); } - let url: URL | undefined; - if (typeof opts.origin === "string" && typeof opts.path === "string") { - url = tryParseURL(opts.origin + opts.path); - } else if (opts.origin instanceof URL) { - if (typeof opts.path === "string") { - url = tryParseURL(opts.origin.href + opts.path); - } else { - url = opts.origin; - } - } + const url = getUrlFromOptions(opts); if (!url) { return orig.apply( @@ -54,31 +62,38 @@ export function wrapDispatch(orig: Dispatch, agent: Agent): Dispatch { ); } - blockRedirectToPrivateIP(url, context, agent); + blockRedirectToPrivateIP(url, context, agent, isFetch); - const port = getPortFromURL(url); + handler.onHeaders = wrapOnHeaders(handler.onHeaders, context, url); - // Wrap onHeaders to check for redirects - handler.onHeaders = wrapOnHeaders( - handler.onHeaders, - { port, url }, - context + // We also pass the incoming context as part of the outgoing request context to prevent context mismatch, if the request is a redirect (argContext is set) + return runWithUndiciRequestContext( + { + port: getPortFromURL(url), + url, + isFetch, + inContext: contextArg, + }, + () => { + return orig.apply( + // @ts-expect-error We dont know the type of this + this, + [opts, handler] + ); + } ); - - return RequestContextStorage.run({ port, url }, () => { - return orig.apply( - // @ts-expect-error We don't know the type of this - this, - [opts, handler] - ); - }); }; } /** * Checks if it's a redirect to a private IP that originates from a user input and blocks it if it is. */ -function blockRedirectToPrivateIP(url: URL, context: Context, agent: Agent) { +function blockRedirectToPrivateIP( + url: URL, + context: Context, + agent: Agent, + isFetch: boolean +) { const isAllowedIP = context && context.remoteAddress && @@ -91,10 +106,12 @@ function blockRedirectToPrivateIP(url: URL, context: Context, agent: Agent) { const found = isRedirectToPrivateIP(url, context); + const operation = isFetch ? "fetch" : "undici.[method]"; + if (found) { agent.onDetectedAttack({ module: "undici", - operation: "fetch", + operation: operation, kind: "ssrf", source: found.source, blocked: agent.shouldBlock(), @@ -110,7 +127,7 @@ function blockRedirectToPrivateIP(url: URL, context: Context, agent: Agent) { if (agent.shouldBlock()) { throw new Error( - `Zen has blocked ${attackKindHumanName("ssrf")}: fetch(...) originating from ${found.source}${escapeHTML((found.pathsToPayload || []).join())}` + `Zen has blocked ${attackKindHumanName("ssrf")}: ${operation}(...) originating from ${found.source}${escapeHTML((found.pathsToPayload || []).join())}` ); } } diff --git a/library/sinks/undici/wrapOnHeaders.ts b/library/sinks/undici/wrapOnHeaders.ts index 153c9bde6..5cb281917 100644 --- a/library/sinks/undici/wrapOnHeaders.ts +++ b/library/sinks/undici/wrapOnHeaders.ts @@ -1,43 +1,44 @@ -import { RequestContextStorage } from "./RequestContextStorage"; import { parseHeaders } from "./parseHeaders"; import { isRedirectStatusCode } from "../../helpers/isRedirectStatusCode"; +import { addRedirectToContext } from "../../vulnerabilities/ssrf/addRedirectToContext"; +import { Context, getContext } from "../../agent/Context"; import type { Dispatcher } from "undici-v6"; -import { Context } from "../../agent/Context"; -import { onRedirect } from "./onRedirect"; +import { getUrlFromOptions } from "./getUrlFromOptions"; type OnHeaders = Dispatcher.DispatchHandlers["onHeaders"]; /** - * Wrap the onHeaders function and check if the response is a redirect. If yes, determine the destination URL and call onRedirect. + * Check if the response is a redirect. If yes, determine the destination URL. */ export function wrapOnHeaders( orig: OnHeaders, - requestContext: ReturnType, - context: Context + context: Context, + requestUrl?: URL ): OnHeaders { - // @ts-expect-error We return undefined if there is no original function, that's fine because the onHeaders function is optional + // @ts-expect-error We return undefined if there is no original function, thats fine because the onHeaders function is optional return function onHeaders() { // eslint-disable-next-line prefer-rest-params const args = Array.from(arguments); - if (args.length > 1) { - const statusCode = args[0]; - if (isRedirectStatusCode(statusCode)) { - try { - // Get redirect location - const headers = parseHeaders(args[1]); - if (typeof headers.location === "string") { - const destinationUrl = new URL(headers.location); - - onRedirect(destinationUrl, requestContext, context); - } - } catch (e) { - // Ignore, log later if we have log levels + try { + let sourceURL = requestUrl; + if (!sourceURL) { + // @ts-expect-error No types for this + sourceURL = getUrlFromOptions(this.opts); + } + + if (sourceURL) { + const destinationUrl = getRedirectDestination(args, sourceURL); + if (destinationUrl) { + addRedirectToContext(sourceURL, destinationUrl, context); } } + } catch { + // Log if we have a logger with log levels } - if (orig) { + // It's a optional function, so we need to check if it was originally defined + if (typeof orig === "function") { return orig.apply( // @ts-expect-error We don't know the type of this this, @@ -48,3 +49,32 @@ export function wrapOnHeaders( } }; } + +function getRedirectDestination( + args: unknown[], + sourceURL: URL +): URL | undefined { + const statusCode = args[0]; + + // Check if the response is a redirect + if (typeof statusCode !== "number" || !isRedirectStatusCode(statusCode)) { + return; + } + + // Get redirect destination + const headers = parseHeaders(args[1] as any); + if (typeof headers.location !== "string") { + return; + } + + // Get the destination URL + return parseLocationHeader(headers.location, sourceURL.origin); +} + +// The location header can be an absolute or relative URL +function parseLocationHeader(header: string, origin: string) { + if (header.startsWith("/")) { + return new URL(header, origin); + } + return new URL(header); +} diff --git a/library/vulnerabilities/ssrf/addRedirectToContext.ts b/library/vulnerabilities/ssrf/addRedirectToContext.ts new file mode 100644 index 000000000..476674861 --- /dev/null +++ b/library/vulnerabilities/ssrf/addRedirectToContext.ts @@ -0,0 +1,54 @@ +import { Context, updateContext } from "../../agent/Context"; +import { getPortFromURL } from "../../helpers/getPortFromURL"; +import { findHostnameInContext } from "./findHostnameInContext"; +import { getRedirectOrigin } from "./getRedirectOrigin"; + +/** + * Adds redirects with user provided hostname / url to the context to prevent SSRF attacks with redirects. + */ +export function addRedirectToContext( + source: URL, + destination: URL, + context: Context +) { + let redirectOrigin: URL | undefined; + + const sourcePort = 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 = findHostnameInContext(source.hostname, context, sourcePort); + + // If the source hostname is not in the context, check if it's a redirect in an 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 = getRedirectOrigin( + context.outgoingRequestRedirects, + source + ); + } + + // If it's 1. an initial redirect with user provided url or 2. a redirect in an existing chain, add it to the context + if (found || redirectOrigin) { + addRedirectToChain(source, destination, context); + } +} + +function addRedirectToChain(source: URL, destination: URL, context: Context) { + const outgoingRedirects = context.outgoingRequestRedirects || []; + const alreadyAdded = outgoingRedirects.find( + (r) => + r.source.toString() === source.toString() && + r.destination.toString() === destination.toString() + ); + + if (alreadyAdded) { + return; + } + + outgoingRedirects.push({ + source, + destination, + }); + + updateContext(context, "outgoingRequestRedirects", outgoingRedirects); +} diff --git a/library/vulnerabilities/ssrf/inspectDNSLookupCalls.ts b/library/vulnerabilities/ssrf/inspectDNSLookupCalls.ts index f6022e1bf..4cfc1b4e1 100644 --- a/library/vulnerabilities/ssrf/inspectDNSLookupCalls.ts +++ b/library/vulnerabilities/ssrf/inspectDNSLookupCalls.ts @@ -10,7 +10,7 @@ import { isPlainObject } from "../../helpers/isPlainObject"; import { getMetadataForSSRFAttack } from "./getMetadataForSSRFAttack"; import { isPrivateIP } from "./isPrivateIP"; import { isIMDSIPAddress, isTrustedHostname } from "./imds"; -import { RequestContextStorage } from "../../sinks/undici/RequestContextStorage"; +import { getUndiciRequestContext } from "../../sinks/undici/RequestContextStorage"; import { findHostnameInContext } from "./findHostnameInContext"; import { getRedirectOrigin } from "./getRedirectOrigin"; import { getPortFromURL } from "../../helpers/getPortFromURL"; @@ -88,7 +88,14 @@ function wrapDNSLookupCallback( return callback(err); } - const context = getContext(); + // This is set if this resolve is part of an outgoing request that we are inspecting + const requestContext = getUndiciRequestContext(); + let context = getContext(); + + // If requestContext.inContext is set, its a request after an redirect. Because the normal context get broken after a redirect, we passed it in the requestContext + if (requestContext?.inContext) { + context = requestContext.inContext; + } if (context) { const matches = agent.getConfig().getEndpoints(context); @@ -121,9 +128,6 @@ function wrapDNSLookupCallback( return callback(err, addresses, family); } - // This is set if this resolve is part of an outgoing request that we are inspecting - const requestContext = RequestContextStorage.getStore(); - let port: number | undefined; if (urlArg) { diff --git a/sample-apps/nextjs-standalone/package-lock.json b/sample-apps/nextjs-standalone/package-lock.json index 086fb53d2..904d76885 100644 --- a/sample-apps/nextjs-standalone/package-lock.json +++ b/sample-apps/nextjs-standalone/package-lock.json @@ -194,9 +194,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001642", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz", - "integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==", + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", "funding": [ { "type": "opencollective", @@ -210,7 +210,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/client-only": { "version": "0.0.1",