Skip to content

Commit eab80c3

Browse files
committed
Refactor DNS result inspection
1 parent 2f793f4 commit eab80c3

File tree

1 file changed

+94
-160
lines changed

1 file changed

+94
-160
lines changed

library/vulnerabilities/ssrf/inspectDNSLookupCalls.ts

Lines changed: 94 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)