Skip to content

Commit 000d17f

Browse files
Merge pull request #484 from AikidoSec/clean-stacktraces
Clean up error stack traces on detected attack
2 parents 64d67c7 + 640294d commit 000d17f

File tree

7 files changed

+59
-11
lines changed

7 files changed

+59
-11
lines changed

library/agent/hooks/onInspectionInterceptorResult.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { attackKindHumanName } from "../Attack";
66
import { getContext, updateContext } from "../Context";
77
import type { InterceptorResult } from "./InterceptorResult";
88
import type { WrapPackageInfo } from "./WrapPackageInfo";
9+
import { cleanError } from "../../helpers/cleanError";
910

1011
// Used for cleaning up the stack trace
1112
const libraryRoot = resolve(__dirname, "../..");
@@ -49,8 +50,10 @@ export function onInspectionInterceptorResult(
4950
});
5051

5152
if (agent.shouldBlock()) {
52-
throw new Error(
53-
`Zen has blocked ${attackKindHumanName(result.kind)}: ${result.operation}(...) originating from ${result.source}${escapeHTML((result.pathsToPayload || []).join())}`
53+
throw cleanError(
54+
new Error(
55+
`Zen has blocked ${attackKindHumanName(result.kind)}: ${result.operation}(...) originating from ${result.source}${escapeHTML((result.pathsToPayload || []).join())}`
56+
)
5457
);
5558
}
5659
}

library/helpers/cleanError.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as t from "tap";
2+
import { cleanError } from "./cleanError";
3+
4+
t.test("it works", async () => {
5+
const error = new Error("test");
6+
t.same(error.message, "test");
7+
t.same(error.name, "Error");
8+
t.same(error.stack!.includes("cleanError.test.ts"), true);
9+
10+
const cleaned = cleanError(new Error("test"));
11+
t.same(cleaned.message, "test");
12+
t.same(cleaned.name, "Error");
13+
t.same(cleaned.stack!.includes("cleanError.test.ts"), false);
14+
15+
const cleaned2 = cleanError(new TypeError("test2"));
16+
t.same(cleaned2.message, "test2");
17+
t.same(cleaned2.name, "TypeError");
18+
t.same(cleaned2.stack!.includes("cleanError.test.ts"), false);
19+
});

library/helpers/cleanError.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { cleanupStackTrace } from "./cleanupStackTrace";
2+
import { getLibraryRoot } from "./getLibraryRoot";
3+
4+
// Cleans up the error stack trace by removing all the lines that are part of the library.
5+
// e.g. useful to hide the module patching if we throw an error on a detected attack.
6+
export function cleanError(err: Error) {
7+
if (err.stack) {
8+
err.stack = cleanupStackTrace(err.stack, getLibraryRoot());
9+
}
10+
11+
return err;
12+
}

library/helpers/getLibraryRoot.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { resolve } from "path";
2+
3+
const libraryRoot = resolve(__dirname, "..");
4+
5+
export function getLibraryRoot(): string {
6+
return libraryRoot;
7+
}

library/sinks/FileSystem.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ t.test("it works", async (t) => {
140140
error.message,
141141
"Zen has blocked a path traversal attack: fs.writeFile(...) originating from body.file.matches"
142142
);
143+
t.same(error.stack!.includes("wrapExport.ts"), false);
143144
}
144145

145146
const error2 = await t.rejects(() =>

library/sinks/undici/wrapDispatch.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { attackKindHumanName } from "../../agent/Attack";
99
import { escapeHTML } from "../../helpers/escapeHTML";
1010
import { isRedirectToPrivateIP } from "../../vulnerabilities/ssrf/isRedirectToPrivateIP";
1111
import { wrapOnHeaders } from "./wrapOnHeaders";
12+
import { cleanError } from "../../helpers/cleanError";
13+
import { cleanupStackTrace } from "../../helpers/cleanupStackTrace";
14+
import { getLibraryRoot } from "../../helpers/getLibraryRoot";
1215

1316
type Dispatch = Dispatcher["dispatch"];
1417

@@ -98,7 +101,7 @@ function blockRedirectToPrivateIP(url: URL, context: Context, agent: Agent) {
98101
kind: "ssrf",
99102
source: found.source,
100103
blocked: agent.shouldBlock(),
101-
stack: new Error().stack!,
104+
stack: cleanupStackTrace(new Error().stack!, getLibraryRoot()),
102105
paths: found.pathsToPayload,
103106
metadata: getMetadataForSSRFAttack({
104107
hostname: found.hostname,
@@ -109,8 +112,10 @@ function blockRedirectToPrivateIP(url: URL, context: Context, agent: Agent) {
109112
});
110113

111114
if (agent.shouldBlock()) {
112-
throw new Error(
113-
`Zen has blocked ${attackKindHumanName("ssrf")}: fetch(...) originating from ${found.source}${escapeHTML((found.pathsToPayload || []).join())}`
115+
throw cleanError(
116+
new Error(
117+
`Zen has blocked ${attackKindHumanName("ssrf")}: fetch(...) originating from ${found.source}${escapeHTML((found.pathsToPayload || []).join())}`
118+
)
114119
);
115120
}
116121
}

library/vulnerabilities/ssrf/inspectDNSLookupCalls.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { isIP, type LookupFunction } from "net";
22
import { LookupAddress } from "dns";
3-
import { resolve } from "path";
43
import { Agent } from "../../agent/Agent";
54
import { attackKindHumanName } from "../../agent/Attack";
65
import { getContext } from "../../agent/Context";
@@ -14,6 +13,8 @@ import { RequestContextStorage } from "../../sinks/undici/RequestContextStorage"
1413
import { findHostnameInContext } from "./findHostnameInContext";
1514
import { getRedirectOrigin } from "./getRedirectOrigin";
1615
import { getPortFromURL } from "../../helpers/getPortFromURL";
16+
import { getLibraryRoot } from "../../helpers/getLibraryRoot";
17+
import { cleanError } from "../../helpers/cleanError";
1718

1819
export function inspectDNSLookupCalls(
1920
lookup: Function,
@@ -189,8 +190,6 @@ function wrapDNSLookupCallback(
189190
return callback(err, addresses, family);
190191
}
191192

192-
const libraryRoot = resolve(__dirname, "../..");
193-
194193
// Used to get the stack trace of the calling location
195194
// We don't throw the error, we just use it to get the stack trace
196195
const stackTraceError = callingLocationStackTrace || new Error();
@@ -201,7 +200,7 @@ function wrapDNSLookupCallback(
201200
kind: "ssrf",
202201
source: found.source,
203202
blocked: agent.shouldBlock(),
204-
stack: cleanupStackTrace(stackTraceError.stack!, libraryRoot),
203+
stack: cleanupStackTrace(stackTraceError.stack!, getLibraryRoot()),
205204
paths: found.pathsToPayload,
206205
metadata: getMetadataForSSRFAttack({ hostname, port }),
207206
request: context,
@@ -210,8 +209,10 @@ function wrapDNSLookupCallback(
210209

211210
if (agent.shouldBlock()) {
212211
return callback(
213-
new Error(
214-
`Zen has blocked ${attackKindHumanName("ssrf")}: ${operation}(...) originating from ${found.source}${escapeHTML((found.pathsToPayload || []).join())}`
212+
cleanError(
213+
new Error(
214+
`Zen has blocked ${attackKindHumanName("ssrf")}: ${operation}(...) originating from ${found.source}${escapeHTML((found.pathsToPayload || []).join())}`
215+
)
215216
)
216217
);
217218
}

0 commit comments

Comments
 (0)