Skip to content

Commit

Permalink
feat: Only build payload paths on attack
Browse files Browse the repository at this point in the history
+ Find multiple occurrences in same source
  • Loading branch information
timokoessler committed Dec 9, 2024
1 parent 7c3c217 commit b563640
Show file tree
Hide file tree
Showing 27 changed files with 317 additions and 215 deletions.
10 changes: 5 additions & 5 deletions library/agent/Agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ t.test("when attack detected", async () => {
operation: "operation",
payload: "payload",
stack: "stack",
path: ".nested",
paths: [".nested"],
metadata: {
db: "app",
},
Expand Down Expand Up @@ -277,7 +277,7 @@ t.test("it checks if user agent is a string", async () => {
payload: "payload",
operation: "operation",
stack: "stack",
path: ".nested",
paths: [".nested"],
metadata: {
db: "app",
},
Expand Down Expand Up @@ -554,7 +554,7 @@ t.test("it logs when failed to report event", async () => {
},
operation: "operation",
stack: "stack",
path: ".nested",
paths: [".nested"],
payload: "payload",
metadata: {
db: "app",
Expand Down Expand Up @@ -636,7 +636,7 @@ t.test("when payload is object", async () => {
operation: "operation",
payload: { $gt: "" },
stack: "stack",
path: ".nested",
paths: [".nested"],
metadata: {
db: "app",
},
Expand Down Expand Up @@ -664,7 +664,7 @@ t.test("when payload is object", async () => {
operation: "operation",
payload: "a".repeat(20000),
stack: "stack",
path: ".nested",
paths: [".nested"],
metadata: {
db: "app",
},
Expand Down
6 changes: 3 additions & 3 deletions library/agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export class Agent {
source,
request,
stack,
path,
paths,
metadata,
payload,
}: {
Expand All @@ -153,7 +153,7 @@ export class Agent {
source: Source;
request: Context;
stack: string;
path: string;
paths: string[];
metadata: Record<string, string>;
payload: unknown;
}) {
Expand All @@ -168,7 +168,7 @@ export class Agent {
module: module,
operation: operation,
blocked: blocked,
path: path,
path: paths.join(),
stack: stack,
source: source,
metadata: limitLengthMetadata(metadata, 4096),
Expand Down
8 changes: 4 additions & 4 deletions library/agent/Context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,24 +110,24 @@ t.test("it clears cache when context is mutated", async (t) => {
t.same(extractStringsFromUserInputCached(getContext()!, "body"), undefined);
t.same(
extractStringsFromUserInputCached(getContext()!, "query"),
new Map(Object.entries({ abc: ".", def: ".abc" }))
new Set(["abc", "def"])
);

updateContext(getContext()!, "query", {});
t.same(extractStringsFromUserInputCached(getContext()!, "body"), undefined);
t.same(
extractStringsFromUserInputCached(getContext()!, "query"),
new Map(Object.entries({}))
new Set()
);

runWithContext({ ...context, body: { a: "z" }, query: { b: "y" } }, () => {
t.same(
extractStringsFromUserInputCached(getContext()!, "body"),
new Map(Object.entries({ a: ".", z: ".a" }))
new Set(["a", "z"])
);
t.same(
extractStringsFromUserInputCached(getContext()!, "query"),
new Map(Object.entries({ b: ".", y: ".b" }))
new Set(["b", "y"])
);
});
});
Expand Down
2 changes: 1 addition & 1 deletion library/agent/applyHooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ t.test("it does not report attack if IP is allowed", async (t) => {
return {
operation: "os.hostname",
source: "body",
pathToPayload: "path",
pathsToPayload: ["path"],
payload: "payload",
metadata: {},
kind: "path_traversal",
Expand Down
2 changes: 1 addition & 1 deletion library/agent/hooks/InterceptorResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type InterceptorResult = {
operation: string;
kind: Kind;
source: Source;
pathToPayload: string;
pathsToPayload: string[];
metadata: Record<string, string>;
payload: unknown;
} | void;
4 changes: 2 additions & 2 deletions library/agent/hooks/wrapExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,15 +180,15 @@ function inspectArgs(
source: result.source,
blocked: agent.shouldBlock(),
stack: cleanupStackTrace(new Error().stack!, libraryRoot),
path: result.pathToPayload,
paths: result.pathsToPayload,
metadata: result.metadata,
request: context,
payload: result.payload,
});

if (agent.shouldBlock()) {
throw new Error(
`Zen has blocked ${attackKindHumanName(result.kind)}: ${result.operation}(...) originating from ${result.source}${escapeHTML(result.pathToPayload)}`
`Zen has blocked ${attackKindHumanName(result.kind)}: ${result.operation}(...) originating from ${result.source}${escapeHTML(result.pathsToPayload.join())}`
);
}
}
Expand Down
59 changes: 59 additions & 0 deletions library/helpers/attackPath.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as t from "tap";
import { getPathsToPayload as get } from "./attackPath";

t.test("it gets paths to payload", async (t) => {
const testObj1 = {
a: {
b: {
c: "payload",
},
},
d: [12, "test", "payload"],
};

t.same(get("payload", testObj1), [".a.b.c", ".d.[2]"]);
t.same(get("test", testObj1), [".d.[1]"]);
t.same(get("notfound", testObj1), []);

t.same(get("payload", "payload"), ["."]);
t.same(get("test", "payload"), []);

t.same(get("", undefined), []);
t.same(get("", null), []);
t.same(
get("", () => {}),
[]
);

t.same(
get("string", [
"string",
1,
true,
null,
undefined,
{ test: "test" },
"string",
]),
[".[0]", ".[6]"]
);

// Concatenates array values
t.same(get("test,test2", ["test", "test2"]), ["."]);
t.same(get("test,test2", { test: { x: ["test", "test2"] } }), [".test.x"]);
});

t.test("it works with jwt", async (t) => {
const testObj2 = {
a: {
x: ["test", "notfoundx"],
b: {
c: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.uG3R-hTeSn-DUddaexdXHw4pvXwKdyxqsD2k0BZrbd4",
},
},
};

t.same(get("John Doe", testObj2), [".a.b.c<jwt>.name"]);
t.same(get("1234567890", testObj2), [".a.b.c<jwt>.sub"]);
t.same(get("notfound", testObj2), []);
});
53 changes: 53 additions & 0 deletions library/helpers/attackPath.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { isPlainObject } from "./isPlainObject";
import { tryDecodeAsJWT } from "./tryDecodeAsJWT";

export type PathPart =
| { type: "jwt" }
| { type: "object"; key: string }
Expand All @@ -24,3 +27,53 @@ export function buildPathToPayload(pathToPayload: PathPart[]): string {
return acc;
}, "");
}

export function getPathsToPayload(
attackPayload: string,
obj: unknown
): string[] {
const matches: string[] = [];

const attackPayloadLowercase = attackPayload.toLowerCase();

const traverse = (value: unknown, path: PathPart[] = []) => {
// Handle strings
if (typeof value === "string") {
if (value.toLowerCase() === attackPayloadLowercase) {
matches.push(buildPathToPayload(path));
return;
}

const jwt = tryDecodeAsJWT(value);
if (jwt.jwt) {
traverse(jwt.object, path.concat({ type: "jwt" }));
}

return;
}

if (Array.isArray(value)) {
// Handle arrays
value.forEach((item, index) => {
traverse(item, path.concat({ type: "array", index }));
});

if (value.join().toLowerCase() === attackPayloadLowercase) {
matches.push(buildPathToPayload(path));
}

return;
}

if (isPlainObject(value)) {
// Handle objects
for (const key in value) {
traverse(value[key], path.concat({ type: "object", key }));
}
}
};

traverse(obj);

return matches;
}
Loading

0 comments on commit b563640

Please sign in to comment.