@@ -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 }  
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 }  
177+             cleanError ( 
178+               new  Error ( 
179+                 `Zen has blocked ${ attackKindHumanName ( "ssrf" ) } ${ operation } ${ 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 } ${ found . source } ${ escapeHTML ( ( found . pathsToPayload   ||   [ ] ) . join ( ) ) }  
214+             `Zen has blocked ${ attackKindHumanName ( "stored_ssrf " ) } ${ operation } 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