Skip to content

Commit

Permalink
Protect new Function(...)
Browse files Browse the repository at this point in the history
  • Loading branch information
timokoessler committed Dec 13, 2024
1 parent aeec6fd commit 1fc6452
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 1 deletion.
9 changes: 8 additions & 1 deletion library/sinks/Eval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const safeContext: Context = {
query: {},
headers: {},
body: {
cakc: "1+ 1",
calc: "1+ 1",
},
cookies: {},
routeParams: {},
Expand Down Expand Up @@ -65,4 +65,11 @@ t.test("it detects JS injections using Eval", async (t) => {
);
}
});

runWithContext(safeContext, () => {
t.same(eval("1 + 1"), 2);
t.same(eval("const x = 1 + 1; x"), 2);
t.same(eval("1 + 1; console.log('hello')"), undefined);
t.same(eval("const test = 1 + 1; console.log('hello')"), undefined);
});
});
113 changes: 113 additions & 0 deletions library/sinks/Function.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import * as t from "tap";
import { runWithContext, type Context } from "../agent/Context";
import { createTestAgent } from "../helpers/createTestAgent";
import { Function as FunctionWrapper } from "./Function";

const dangerousContext: Context = {
remoteAddress: "::1",
method: "POST",
url: "http://localhost:4000",
query: {},
headers: {},
body: {
calc: "1 + 1; console.log('hello')",
},
cookies: {},
routeParams: {},
source: "express",
route: "/posts/:id",
};

const safeContext: Context = {
remoteAddress: "::1",
method: "POST",
url: "http://localhost:4000/",
query: {},
headers: {},
body: {
calc: "1+ 1",
},
cookies: {},
routeParams: {},
source: "express",
route: "/posts/:id",
};

t.test("it detects JS injections using Function", async (t) => {
const agent = createTestAgent();
agent.start([new FunctionWrapper()]);

t.same(new Function("return 1 + 1")(), 2);
t.same(new Function("1 + 1")(), undefined);
t.same(new Function("const x = 1 + 1; return x")(), 2);

t.same(
new Function(
"const sumArray = (arr) => arr.reduce((previousValue, currentValue) => previousValue + currentValue); return sumArray"
)()([1, 2, 3]),
6
);

t.same(
Function(
"function findLargestNumber (arr) { return Math.max(...arr) }; return findLargestNumber"
)
.call({})
.call({}, [2, 4, 1, 8, 5]),
8
);
t.same(new Function("a", "b", "return a + b")(2, 6), 8);
t.same(new Function("inp = 9", "const test = inp; return test;")(), 9);
t.same(new Function("a, b", "c = 5", "return a + b + c")(2, 6), 13);

const error1 = t.throws(() => new Function("/*", "*/) {"));
t.ok(error1 instanceof Error);
if (error1 instanceof Error) {
t.same(error1.message, "Unexpected end of arg string");
}

runWithContext(safeContext, () => {
t.same(new Function("1 + 1")(), undefined);
t.same(new Function("const x = 1 + 1; return x")(), 2);
});

runWithContext(dangerousContext, () => {
t.same(new Function("1 + 1")(), undefined);
t.same(new Function("const x = 1 + 1; return x")(), 2);

const error = t.throws(() => new Function("1 + 1; console.log('hello')"));
t.ok(error instanceof Error);
if (error instanceof Error) {
t.same(
error.message,
"Zen has blocked a JavaScript injection: new Function(...) originating from body.calc"
);
}

const error2 = t.throws(
() => new Function("const test = 1 + 1; console.log('hello')")
);
t.ok(error2 instanceof Error);
if (error2 instanceof Error) {
t.same(
error2.message,
"Zen has blocked a JavaScript injection: new Function(...) originating from body.calc"
);
}

const error3 = t.throws(() =>
new Function(
"a, b",
"c = 5",
"const x = a + b + c + 1 + 1; console.log('hello'); return x;"
)(2, 6)
);
t.ok(error2 instanceof Error);
if (error2 instanceof Error) {
t.same(
error2.message,
"Zen has blocked a JavaScript injection: new Function(...) originating from body.calc"
);
}
});
});
41 changes: 41 additions & 0 deletions library/sinks/Function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { getContext } from "../agent/Context";
import { Hooks } from "../agent/hooks/Hooks";
import { Wrapper } from "../agent/Wrapper";
import { checkContextForJsInjection } from "../vulnerabilities/js-injection/checkContextForJsInjection";

export class Function implements Wrapper {
private inspectFunction(args: any[]) {
const context = getContext();

if (!context || !Array.isArray(args) || args.length === 0) {
return undefined;
}

const findLastStringArg = (args: any[]) => {
for (let i = args.length - 1; i >= 0; --i) {
if (typeof args[i] === "string") {
return args[i];
}
}
return undefined;
};

const lastStringArg = findLastStringArg(args);

if (lastStringArg) {
return checkContextForJsInjection({
js: lastStringArg,
operation: "new Function",
context,
});
}

return undefined;
}

wrap(hooks: Hooks) {
hooks.addGlobal("Function", {
inspectArgs: this.inspectFunction,
});
}
}

0 comments on commit 1fc6452

Please sign in to comment.