@@ -90,6 +90,14 @@ function wrapDNSLookupCallback(
9090 }
9191
9292 const context = getContext ( ) ;
93+ const resolvedIPAddresses = getResolvedIPAddresses ( addresses ) ;
94+
95+ const privateIP = resolvedIPAddresses . find ( isPrivateIP ) ;
96+ if ( ! privateIP ) {
97+ // If the hostname doesn't resolve to a private IP address, it's not an SSRF attack
98+ // Just call the original callback to allow the DNS lookup
99+ return callback ( err , addresses , family ) ;
100+ }
93101
94102 if ( context ) {
95103 const matches = agent . getConfig ( ) . getEndpoints ( context ) ;
@@ -99,154 +107,114 @@ function wrapDNSLookupCallback(
99107 // Just call the original callback to allow the DNS lookup
100108 return callback ( err , addresses , family ) ;
101109 }
102- }
103-
104- const resolvedIPAddresses = getResolvedIPAddresses ( addresses ) ;
105-
106- const imdsIpResult = resolvesToIMDSIP ( resolvedIPAddresses , hostname ) ;
107- if ( ! context && imdsIpResult . isIMDS ) {
108- reportStoredImdsIpSSRF ( {
109- agent,
110- module,
111- operation,
112- hostname,
113- privateIp : imdsIpResult . ip ,
114- callingLocationStackTrace,
115- } ) ;
116-
117- // Block stored SSRF attack that target IMDS IP addresses
118- // An attacker could have stored a hostname in a database that points to an IMDS IP address
119- // We don't check if the user input contains the hostname because there's no context
120- if ( agent . shouldBlock ( ) ) {
121- return callback (
122- new Error (
123- `Zen has blocked ${ attackKindHumanName ( "stored_ssrf" ) } : ${ operation } (...) originating from unknown source`
124- )
125- ) ;
126- }
127- }
128-
129- if ( ! context ) {
130- // If there's no context, we can't check if the hostname is in the context
131- // Just call the original callback to allow the DNS lookup
132- return callback ( err , addresses , family ) ;
133- }
134110
135- // This is set if this resolve is part of an outgoing request that we are inspecting
136- const requestContext = RequestContextStorage . getStore ( ) ;
111+ const isBypassedIP =
112+ context . remoteAddress &&
113+ agent . getConfig ( ) . isBypassedIP ( context . remoteAddress ) ;
137114
138- let port : number | undefined ;
139-
140- if ( urlArg ) {
141- port = getPortFromURL ( urlArg ) ;
142- } else if ( requestContext ) {
143- port = requestContext . port ;
144- }
145-
146- const privateIP = resolvedIPAddresses . find ( isPrivateIP ) ;
147-
148- if ( ! privateIP ) {
149- // If the hostname doesn't resolve to a private IP address, it's not an SSRF attack
150- // Just call the original callback to allow the DNS lookup
151- return callback ( err , addresses , family ) ;
152- }
153-
154- let found = findHostnameInContext ( hostname , context , port ) ;
155-
156- // The hostname is not found in the context, check if it's a redirect
157- if ( ! found && context . outgoingRequestRedirects ) {
158- let url : URL | undefined ;
159- // Url arg is passed when wrapping node:http(s), but not for undici / fetch because of the way they are wrapped
160- // For undici / fetch we need to get the url from the request context, which is an additional async context for outgoing requests,
161- // not to be confused with the "normal" context used in wide parts of this library
162- if ( urlArg ) {
163- url = urlArg ;
164- } else if ( requestContext ) {
165- url = new URL ( requestContext . url ) ;
115+ if ( isBypassedIP ) {
116+ // If the IP address is allowed, we don't need to block the request
117+ // Just call the original callback to allow the DNS lookup
118+ return callback ( err , addresses , family ) ;
166119 }
167120
168- if ( url ) {
169- // Get the origin of the redirect chain (the first URL in the chain), if the URL is the result of a redirect
170- const redirectOrigin = getRedirectOrigin (
171- context . outgoingRequestRedirects ,
172- url
173- ) ;
121+ // This is set if this resolve is part of an outgoing request that we are inspecting
122+ const requestContext = RequestContextStorage . getStore ( ) ;
123+ const port = urlArg ? getPortFromURL ( urlArg ) : requestContext ?. port ;
124+
125+ let found = findHostnameInContext ( hostname , context , port ) ;
126+
127+ // The hostname is not found in the context, check if it's a redirect
128+ if ( ! found && context . outgoingRequestRedirects ) {
129+ let url : URL | undefined ;
130+ // Url arg is passed when wrapping node:http(s), but not for undici / fetch because of the way they are wrapped
131+ // For undici / fetch we need to get the url from the request context, which is an additional async context for outgoing requests,
132+ // not to be confused with the "normal" context used in wide parts of this library
133+ if ( urlArg ) {
134+ url = urlArg ;
135+ } else if ( requestContext ) {
136+ url = new URL ( requestContext . url ) ;
137+ }
174138
175- // If the URL is the result of a redirect, get the origin of the redirect chain for reporting the attack source
176- if ( redirectOrigin ) {
177- found = findHostnameInContext (
178- redirectOrigin . hostname ,
179- context ,
180- getPortFromURL ( redirectOrigin )
139+ if ( url ) {
140+ // Get the origin of the redirect chain (the first URL in the chain), if the URL is the result of a redirect
141+ const redirectOrigin = getRedirectOrigin (
142+ context . outgoingRequestRedirects ,
143+ url
181144 ) ;
145+
146+ // If the URL is the result of a redirect, get the origin of the redirect chain for reporting the attack source
147+ if ( redirectOrigin ) {
148+ found = findHostnameInContext (
149+ redirectOrigin . hostname ,
150+ context ,
151+ getPortFromURL ( redirectOrigin )
152+ ) ;
153+ }
182154 }
183155 }
184- }
185156
186- if ( ! found ) {
187- if ( imdsIpResult . isIMDS ) {
188- // Stored SSRF attack executed during another request (context set)
189- reportStoredImdsIpSSRF ( {
190- agent,
191- module,
192- operation,
193- hostname,
194- privateIp : imdsIpResult . ip ,
195- callingLocationStackTrace,
157+ if ( found ) {
158+ // Used to get the stack trace of the calling location
159+ // We don't throw the error, we just use it to get the stack trace
160+ const stackTraceError = callingLocationStackTrace || new Error ( ) ;
161+
162+ agent . onDetectedAttack ( {
163+ module : module ,
164+ operation : operation ,
165+ kind : "ssrf" ,
166+ source : found . source ,
167+ blocked : agent . shouldBlock ( ) ,
168+ stack : cleanupStackTrace ( stackTraceError . stack ! , getLibraryRoot ( ) ) ,
169+ paths : found . pathsToPayload ,
170+ metadata : getMetadataForSSRFAttack ( { hostname, port, privateIP } ) ,
171+ request : context ,
172+ payload : found . payload ,
196173 } ) ;
197174
198- // Block stored SSRF attack that target IMDS IP addresses
199- // An attacker could have stored a hostname in a database that points to an IMDS IP address
200175 if ( agent . shouldBlock ( ) ) {
201176 return callback (
202- new Error (
203- `Zen has blocked ${ attackKindHumanName ( "stored_ssrf" ) } : ${ operation } (...) originating from unknown source`
177+ cleanError (
178+ new Error (
179+ `Zen has blocked ${ attackKindHumanName ( "ssrf" ) } : ${ operation } (...) originating from ${ found . source } ${ escapeHTML ( ( found . pathsToPayload || [ ] ) . join ( ) ) } `
180+ )
204181 )
205182 ) ;
206183 }
207184 }
208-
209- // If we can't find the hostname in the context, it's not an SSRF attack
210- // Just call the original callback to allow the DNS lookup
211- return callback ( err , addresses , family ) ;
212185 }
213186
214- const isBypassedIP =
215- context &&
216- context . remoteAddress &&
217- agent . getConfig ( ) . isBypassedIP ( context . remoteAddress ) ;
218-
219- if ( isBypassedIP ) {
220- // If the IP address is allowed, we don't need to block the request
221- // Just call the original callback to allow the DNS lookup
222- return callback ( err , addresses , family ) ;
223- }
224-
225- // Used to get the stack trace of the calling location
226- // We don't throw the error, we just use it to get the stack trace
227- const stackTraceError = callingLocationStackTrace || new Error ( ) ;
228-
229- agent . onDetectedAttack ( {
230- module : module ,
231- operation : operation ,
232- kind : "ssrf" ,
233- source : found . source ,
234- blocked : agent . shouldBlock ( ) ,
235- stack : cleanupStackTrace ( stackTraceError . stack ! , getLibraryRoot ( ) ) ,
236- paths : found . pathsToPayload ,
237- metadata : getMetadataForSSRFAttack ( { hostname, port, privateIP } ) ,
238- request : context ,
239- payload : found . payload ,
240- } ) ;
187+ // Check for stored IMDS SSRF attack
188+ const imdsIpResult = resolvesToIMDSIP ( resolvedIPAddresses , hostname ) ;
189+ if ( imdsIpResult . isIMDS ) {
190+ const stackTraceError = callingLocationStackTrace || new Error ( ) ;
191+ agent . onDetectedAttack ( {
192+ module : module ,
193+ operation : operation ,
194+ kind : "stored_ssrf" ,
195+ source : undefined ,
196+ blocked : agent . shouldBlock ( ) ,
197+ stack : cleanupStackTrace ( stackTraceError . stack ! , getLibraryRoot ( ) ) ,
198+ paths : [ ] ,
199+ metadata : getMetadataForSSRFAttack ( {
200+ hostname,
201+ port : undefined ,
202+ privateIP : imdsIpResult . ip ,
203+ } ) ,
204+ request : undefined ,
205+ payload : undefined ,
206+ } ) ;
241207
242- if ( agent . shouldBlock ( ) ) {
243- return callback (
244- cleanError (
208+ // Block stored SSRF attack that target IMDS IP addresses
209+ // An attacker could have stored a hostname in a database that points to an IMDS IP address
210+ // We don't check if the user input contains the hostname because there's no context
211+ if ( agent . shouldBlock ( ) ) {
212+ return callback (
245213 new Error (
246- `Zen has blocked ${ attackKindHumanName ( "ssrf " ) } : ${ operation } (...) originating from ${ found . source } ${ escapeHTML ( ( found . pathsToPayload || [ ] ) . join ( ) ) } `
214+ `Zen has blocked ${ attackKindHumanName ( "stored_ssrf " ) } : ${ operation } (...) originating from unknown source `
247215 )
248- )
249- ) ;
216+ ) ;
217+ }
250218 }
251219
252220 // If the attack should not be blocked
@@ -295,37 +263,3 @@ function resolvesToIMDSIP(
295263 isIMDS : false ,
296264 } ;
297265}
298-
299- function reportStoredImdsIpSSRF ( {
300- agent,
301- callingLocationStackTrace,
302- module,
303- operation,
304- hostname,
305- privateIp,
306- } : {
307- agent : Agent ;
308- callingLocationStackTrace ?: Error ;
309- module : string ;
310- operation : string ;
311- hostname : string ;
312- privateIp : string ;
313- } ) {
314- const stackTraceError = callingLocationStackTrace || new Error ( ) ;
315- agent . onDetectedAttack ( {
316- module : module ,
317- operation : operation ,
318- kind : "stored_ssrf" ,
319- source : undefined ,
320- blocked : agent . shouldBlock ( ) ,
321- stack : cleanupStackTrace ( stackTraceError . stack ! , getLibraryRoot ( ) ) ,
322- paths : [ ] ,
323- metadata : getMetadataForSSRFAttack ( {
324- hostname,
325- port : undefined ,
326- privateIP : privateIp ,
327- } ) ,
328- request : undefined ,
329- payload : undefined ,
330- } ) ;
331- }
0 commit comments