Skip to content

Commit 2f793f4

Browse files
authored
Merge pull request #734 from AikidoSec/report-stored-ssrf
Report stored SSRF as attack event
2 parents b715fe7 + f00a765 commit 2f793f4

File tree

6 files changed

+328
-36
lines changed

6 files changed

+328
-36
lines changed

library/agent/Agent.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,29 @@ export class Agent {
199199
operation: string;
200200
kind: Kind;
201201
blocked: boolean;
202-
source: Source;
203-
request: Context;
202+
source: Source | undefined;
203+
request: Context | undefined;
204204
stack: string;
205205
paths: string[];
206206
metadata: Record<string, string>;
207207
payload: unknown;
208208
}) {
209+
const attackRequest: DetectedAttack["request"] = request
210+
? {
211+
method: request.method,
212+
url: request.url,
213+
ipAddress: request.remoteAddress,
214+
userAgent:
215+
typeof request.headers["user-agent"] === "string"
216+
? request.headers["user-agent"]
217+
: undefined,
218+
body: convertRequestBodyToString(request.body),
219+
headers: filterEmptyRequestHeaders(request.headers),
220+
source: request.source,
221+
route: request.route,
222+
}
223+
: undefined;
224+
209225
const attack: DetectedAttack = {
210226
type: "detected_attack",
211227
time: Date.now(),
@@ -218,22 +234,13 @@ export class Agent {
218234
source: source,
219235
metadata: limitLengthMetadata(metadata, 4096),
220236
kind: kind,
221-
payload: JSON.stringify(payload).substring(0, 4096),
222-
user: request.user,
223-
},
224-
request: {
225-
method: request.method,
226-
url: request.url,
227-
ipAddress: request.remoteAddress,
228-
userAgent:
229-
typeof request.headers["user-agent"] === "string"
230-
? request.headers["user-agent"]
237+
payload:
238+
payload !== undefined
239+
? JSON.stringify(payload).substring(0, 4096)
231240
: undefined,
232-
body: convertRequestBodyToString(request.body),
233-
headers: filterEmptyRequestHeaders(request.headers),
234-
source: request.source,
235-
route: request.route,
241+
user: request?.user,
236242
},
243+
request: attackRequest,
237244
agent: this.getAgentInfo(),
238245
};
239246

library/agent/Attack.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type Kind =
44
| "shell_injection"
55
| "path_traversal"
66
| "ssrf"
7+
| "stored_ssrf"
78
| "code_injection";
89

910
export function attackKindHumanName(kind: Kind) {
@@ -18,6 +19,8 @@ export function attackKindHumanName(kind: Kind) {
1819
return "a path traversal attack";
1920
case "ssrf":
2021
return "a server-side request forgery";
22+
case "stored_ssrf":
23+
return "a stored server-side request forgery";
2124
case "code_injection":
2225
return "a JavaScript injection";
2326
}

library/agent/AttackLogger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class AttackLogger {
2828
this.logCount++; // Increment the log counter
2929

3030
const { blocked, kind, operation, source, path } = event.attack;
31-
const { ipAddress } = event.request;
31+
const ipAddress = event.request?.ipAddress;
3232

3333
const message = `Zen has ${blocked ? "blocked" : "detected"} ${attackKindHumanName(kind)}: kind="${escapeLog(kind)}" operation="${escapeLog(operation)}(...)" source="${escapeLog(source)}${escapeLog(path)}" ip="${escapeLog(ipAddress)}"`;
3434

library/agent/api/Event.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,27 @@ export type User = {
3939

4040
export type DetectedAttack = {
4141
type: "detected_attack";
42-
request: {
43-
method: string | undefined;
44-
ipAddress: string | undefined;
45-
userAgent: string | undefined;
46-
url: string | undefined;
47-
headers: Record<string, string | string[]>;
48-
body: string | undefined;
49-
source: string;
50-
route: string | undefined;
51-
};
42+
request:
43+
| {
44+
method: string | undefined;
45+
ipAddress: string | undefined;
46+
userAgent: string | undefined;
47+
url: string | undefined;
48+
headers: Record<string, string | string[]>;
49+
body: string | undefined;
50+
source: string;
51+
route: string | undefined;
52+
}
53+
| undefined;
5254
attack: {
5355
kind: Kind;
5456
operation: string;
5557
module: string;
5658
blocked: boolean;
57-
source: Source;
59+
source: Source | undefined;
5860
path: string;
5961
stack: string;
60-
payload: string;
62+
payload: string | undefined;
6163
metadata: Record<string, string>;
6264
user: User | undefined;
6365
};

library/vulnerabilities/ssrf/inspectDNSLookupCalls.test.ts

Lines changed: 204 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ t.test("Blocks IMDS SSRF with untrusted domain", async (t) => {
370370
if (err instanceof Error) {
371371
t.same(
372372
err.message,
373-
"Zen has blocked a server-side request forgery: operation(...) originating from unknown source"
373+
"Zen has blocked a stored server-side request forgery: operation(...) originating from unknown source"
374374
);
375375
}
376376
t.same(address, undefined);
@@ -384,7 +384,7 @@ t.test("Blocks IMDS SSRF with untrusted domain", async (t) => {
384384
if (err instanceof Error) {
385385
t.same(
386386
err.message,
387-
"Zen has blocked a server-side request forgery: operation(...) originating from unknown source"
387+
"Zen has blocked a stored server-side request forgery: operation(...) originating from unknown source"
388388
);
389389
}
390390
t.same(address, undefined);
@@ -524,3 +524,205 @@ t.test("it ignores when the argument is an IP address", async (t) => {
524524
}),
525525
]);
526526
});
527+
528+
t.test("Reports stored SSRF without context", async (t) => {
529+
const api = new ReportingAPIForTesting();
530+
const agent = createTestAgent({
531+
token: new Token("123"),
532+
api,
533+
});
534+
agent.start([]);
535+
536+
const wrappedLookup = inspectDNSLookupCalls(
537+
imdsMockLookup,
538+
agent,
539+
"http",
540+
"request"
541+
);
542+
543+
await new Promise<void>((resolve) => {
544+
wrappedLookup("imds.test.com", { family: 4 }, (err, address) => {
545+
t.same(err instanceof Error, true);
546+
if (err instanceof Error) {
547+
t.same(
548+
err.message,
549+
"Zen has blocked a stored server-side request forgery: request(...) originating from unknown source"
550+
);
551+
}
552+
t.same(address, undefined);
553+
resolve();
554+
});
555+
});
556+
557+
t.match(api.getEvents(), [
558+
{
559+
type: "started",
560+
},
561+
{
562+
type: "detected_attack",
563+
attack: {
564+
kind: "stored_ssrf",
565+
operation: "request",
566+
module: "http",
567+
blocked: true,
568+
source: undefined,
569+
path: "",
570+
payload: undefined,
571+
metadata: {
572+
hostname: "imds.test.com",
573+
privateIP: "169.254.169.254",
574+
},
575+
user: undefined,
576+
},
577+
request: undefined,
578+
},
579+
]);
580+
});
581+
582+
t.test("Reports stored SSRF with context set", async (t) => {
583+
const api = new ReportingAPIForTesting();
584+
const agent = createTestAgent({
585+
token: new Token("123"),
586+
api,
587+
});
588+
agent.start([]);
589+
590+
await runWithContext(
591+
{
592+
remoteAddress: "::1",
593+
method: "POST",
594+
url: "http://app.example.com:4000",
595+
query: {},
596+
headers: {},
597+
body: {
598+
image: "test.png",
599+
},
600+
cookies: {},
601+
routeParams: {},
602+
source: "express",
603+
route: "/posts/:id",
604+
},
605+
async () => {
606+
const wrappedLookup = inspectDNSLookupCalls(
607+
imdsMockLookup,
608+
agent,
609+
"http",
610+
"request"
611+
);
612+
613+
await new Promise<void>((resolve) => {
614+
wrappedLookup("imds.test.com", { family: 4 }, (err, address) => {
615+
t.same(err instanceof Error, true);
616+
if (err instanceof Error) {
617+
t.same(
618+
err.message,
619+
"Zen has blocked a stored server-side request forgery: request(...) originating from unknown source"
620+
);
621+
}
622+
t.same(address, undefined);
623+
resolve();
624+
});
625+
});
626+
}
627+
);
628+
629+
t.match(api.getEvents(), [
630+
{
631+
type: "started",
632+
},
633+
{
634+
type: "detected_attack",
635+
attack: {
636+
kind: "stored_ssrf",
637+
operation: "request",
638+
module: "http",
639+
blocked: true,
640+
source: undefined,
641+
path: "",
642+
payload: undefined,
643+
metadata: {
644+
hostname: "imds.test.com",
645+
privateIP: "169.254.169.254",
646+
},
647+
user: undefined,
648+
},
649+
request: undefined,
650+
},
651+
]);
652+
});
653+
654+
t.test("Reports IDMS SSRF from current request context", async (t) => {
655+
const api = new ReportingAPIForTesting();
656+
const agent = createTestAgent({
657+
token: new Token("123"),
658+
api,
659+
});
660+
agent.start([]);
661+
662+
await runWithContext(
663+
{
664+
remoteAddress: "::1",
665+
method: "POST",
666+
url: "http://app.example.com:4000",
667+
query: {},
668+
headers: {},
669+
body: {
670+
image: "https://imds.test.com",
671+
},
672+
cookies: {},
673+
routeParams: {},
674+
source: "express",
675+
route: "/posts/:id",
676+
},
677+
async () => {
678+
const wrappedLookup = inspectDNSLookupCalls(
679+
imdsMockLookup,
680+
agent,
681+
"http",
682+
"request"
683+
);
684+
685+
await new Promise<void>((resolve) => {
686+
wrappedLookup("imds.test.com", { family: 4 }, (err, address) => {
687+
t.same(err instanceof Error, true);
688+
if (err instanceof Error) {
689+
t.same(
690+
err.message,
691+
"Zen has blocked a server-side request forgery: request(...) originating from body.image"
692+
);
693+
}
694+
t.same(address, undefined);
695+
resolve();
696+
});
697+
});
698+
}
699+
);
700+
701+
t.match(api.getEvents(), [
702+
{
703+
type: "started",
704+
},
705+
{
706+
type: "detected_attack",
707+
attack: {
708+
kind: "ssrf",
709+
operation: "request",
710+
module: "http",
711+
blocked: true,
712+
source: "body",
713+
path: ".image",
714+
payload: "https://imds.test.com",
715+
metadata: {
716+
hostname: "imds.test.com",
717+
privateIP: "169.254.169.254",
718+
},
719+
user: undefined,
720+
},
721+
request: {
722+
method: "POST",
723+
ipAddress: "::1",
724+
url: "http://app.example.com:4000",
725+
},
726+
},
727+
]);
728+
});

0 commit comments

Comments
 (0)