Skip to content

Commit 8df835e

Browse files
committed
Always check if IP resolves to IMDS
Whether there is context or not Unless endpoint protection is off
1 parent 6a59654 commit 8df835e

File tree

2 files changed

+139
-46
lines changed

2 files changed

+139
-46
lines changed

library/vulnerabilities/ssrf/inspectDNSLookupCalls.test.ts

Lines changed: 116 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ t.test(
161161

162162
t.test(
163163
"it does not block resolved private IP if endpoint protection is turned off",
164-
(t) => {
164+
async (t) => {
165165
const logger = new LoggerNoop();
166166
const api = new ReportingAPIForTesting({
167167
success: true,
@@ -185,6 +185,9 @@ t.test(
185185
const token = new Token("123");
186186
const agent = new Agent(true, logger, api, token, undefined);
187187
agent.start([]);
188+
189+
await new Promise((resolve) => setTimeout(resolve, 0));
190+
188191
api.clear();
189192

190193
const wrappedLookup = inspectDNSLookupCalls(
@@ -194,15 +197,17 @@ t.test(
194197
"operation"
195198
);
196199

197-
runWithContext(context, () => {
198-
wrappedLookup("localhost", (err, address) => {
199-
t.same(err, null);
200-
t.same(
201-
address,
202-
process.version.startsWith("v16") ? "127.0.0.1" : "::1"
203-
);
204-
t.same(api.getEvents(), []);
205-
t.end();
200+
await new Promise<void>((resolve) => {
201+
runWithContext(context, () => {
202+
wrappedLookup("localhost", (err, address) => {
203+
t.same(err, null);
204+
t.same(
205+
address,
206+
process.version.startsWith("v16") ? "127.0.0.1" : "::1"
207+
);
208+
t.same(api.getEvents(), []);
209+
resolve();
210+
});
206211
});
207212
});
208213
}
@@ -330,7 +335,7 @@ const imdsMockLookup = (
330335
return lookup(hostname, options, callback);
331336
};
332337

333-
t.test("Blocks IMDS SSRF with untrusted domain", (t) => {
338+
t.test("Blocks IMDS SSRF with untrusted domain", async (t) => {
334339
const logger = new LoggerNoop();
335340
const api = new ReportingAPIForTesting();
336341
const token = new Token("123");
@@ -344,18 +349,82 @@ t.test("Blocks IMDS SSRF with untrusted domain", (t) => {
344349
"operation"
345350
);
346351

347-
wrappedLookup("imds.test.com", { family: 4 }, (err, addresses) => {
348-
t.same(err instanceof Error, true);
349-
t.same(
350-
err.message,
351-
"Aikido firewall has blocked a server-side request forgery: operation(...) originating from unknown source"
352-
);
353-
t.same(addresses, undefined);
354-
t.end();
355-
});
352+
await Promise.all([
353+
new Promise<void>((resolve) => {
354+
wrappedLookup("imds.test.com", { family: 4 }, (err, addresses) => {
355+
t.same(err instanceof Error, true);
356+
t.same(
357+
err.message,
358+
"Aikido firewall has blocked a server-side request forgery: operation(...) originating from unknown source"
359+
);
360+
t.same(addresses, undefined);
361+
resolve();
362+
});
363+
}),
364+
new Promise<void>((resolve) => {
365+
runWithContext(context, () => {
366+
wrappedLookup("imds.test.com", { family: 4 }, (err, addresses) => {
367+
t.same(err instanceof Error, true);
368+
t.same(
369+
err.message,
370+
"Aikido firewall has blocked a server-side request forgery: operation(...) originating from unknown source"
371+
);
372+
t.same(addresses, undefined);
373+
resolve();
374+
});
375+
});
376+
}),
377+
]);
356378
});
357379

358-
t.test("Does not block IMDS SSRF with Google metadata domain", (t) => {
380+
t.test(
381+
"it ignores IMDS SSRF with untrusted domain when endpoint protection is force off",
382+
async (t) => {
383+
const logger = new LoggerNoop();
384+
const api = new ReportingAPIForTesting({
385+
success: true,
386+
heartbeatIntervalInMS: 10 * 60 * 1000,
387+
endpoints: [
388+
{
389+
method: "POST",
390+
route: "/posts/:id",
391+
forceProtectionOff: true,
392+
rateLimiting: {
393+
enabled: false,
394+
windowSizeInMS: 60 * 1000,
395+
maxRequests: 100,
396+
},
397+
},
398+
],
399+
blockedUserIds: [],
400+
allowedIPAddresses: [],
401+
configUpdatedAt: 0,
402+
});
403+
const token = new Token("123");
404+
const agent = new Agent(true, logger, api, token, undefined);
405+
agent.start([]);
406+
407+
// Wait for the agent to start
408+
await new Promise((resolve) => setTimeout(resolve, 0));
409+
410+
const wrappedLookup = inspectDNSLookupCalls(
411+
imdsMockLookup,
412+
agent,
413+
"module",
414+
"operation"
415+
);
416+
417+
runWithContext(context, () => {
418+
wrappedLookup("imds.test.com", { family: 4 }, (err, addresses) => {
419+
t.same(err, null);
420+
t.same(addresses, "169.254.169.254");
421+
t.end();
422+
});
423+
});
424+
}
425+
);
426+
427+
t.test("Does not block IMDS SSRF with Google metadata domain", async (t) => {
359428
const logger = new LoggerNoop();
360429
const api = new ReportingAPIForTesting();
361430
const token = new Token("123");
@@ -369,11 +438,32 @@ t.test("Does not block IMDS SSRF with Google metadata domain", (t) => {
369438
"operation"
370439
);
371440

372-
wrappedLookup("metadata.google.internal", { family: 4 }, (err, addresses) => {
373-
t.same(err, null);
374-
t.same(addresses, "169.254.169.254");
375-
t.end();
376-
});
441+
await Promise.all([
442+
new Promise<void>((resolve) => {
443+
wrappedLookup(
444+
"metadata.google.internal",
445+
{ family: 4 },
446+
(err, addresses) => {
447+
t.same(err, null);
448+
t.same(addresses, "169.254.169.254");
449+
resolve();
450+
}
451+
);
452+
}),
453+
new Promise<void>((resolve) => {
454+
runWithContext(context, () => {
455+
wrappedLookup(
456+
"metadata.google.internal",
457+
{ family: 4 },
458+
(err, addresses) => {
459+
t.same(err, null);
460+
t.same(addresses, "169.254.169.254");
461+
resolve();
462+
}
463+
);
464+
});
465+
}),
466+
]);
377467
});
378468

379469
t.test("it ignores when the argument is an IP address", async (t) => {

library/vulnerabilities/ssrf/inspectDNSLookupCalls.ts

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -76,31 +76,34 @@ function wrapDNSLookupCallback(
7676
}
7777

7878
const context = getContext();
79-
const resolvedIPAddresses = getResolvedIPAddresses(addresses);
8079

81-
if (!context) {
82-
if (resolvesToIMDSIP(resolvedIPAddresses, hostname)) {
83-
// Block stored SSRF attack that target IMDS IP addresses
84-
// An attacker could have stored a hostname in a database that points to an IMDS IP address
85-
// We don't check if the user input contains the hostname because there's no context
86-
if (agent.shouldBlock()) {
87-
return callback(
88-
new Error(
89-
`Aikido firewall has blocked ${attackKindHumanName("ssrf")}: ${operation}(...) originating from unknown source`
90-
)
91-
);
92-
}
93-
}
80+
if (context) {
81+
const endpoint = agent.getConfig().getEndpoint(context);
9482

95-
// If there's no context and the hostname doesn't resolve to an IMDS IP address, we don't need to inspect the resolved IPs
96-
// Just call the original callback to allow the DNS lookup
97-
return callback(err, addresses, family);
83+
if (endpoint && endpoint.endpoint.forceProtectionOff) {
84+
// User disabled protection for this endpoint, we don't need to inspect the resolved IPs
85+
// Just call the original callback to allow the DNS lookup
86+
return callback(err, addresses, family);
87+
}
9888
}
9989

100-
const endpoint = agent.getConfig().getEndpoint(context);
90+
const resolvedIPAddresses = getResolvedIPAddresses(addresses);
91+
92+
if (resolvesToIMDSIP(resolvedIPAddresses, hostname)) {
93+
// Block stored SSRF attack that target IMDS IP addresses
94+
// An attacker could have stored a hostname in a database that points to an IMDS IP address
95+
// We don't check if the user input contains the hostname because there's no context
96+
if (agent.shouldBlock()) {
97+
return callback(
98+
new Error(
99+
`Aikido firewall has blocked ${attackKindHumanName("ssrf")}: ${operation}(...) originating from unknown source`
100+
)
101+
);
102+
}
103+
}
101104

102-
if (endpoint && endpoint.endpoint.forceProtectionOff) {
103-
// User disabled protection for this endpoint, we don't need to inspect the resolved IPs
105+
if (!context) {
106+
// If there's no context, we can't check if the hostname is in the context
104107
// Just call the original callback to allow the DNS lookup
105108
return callback(err, addresses, family);
106109
}

0 commit comments

Comments
 (0)