Skip to content

Commit

Permalink
Merge branch 'js-injections' into mongodb-js-injection
Browse files Browse the repository at this point in the history
  • Loading branch information
timokoessler committed Dec 16, 2024
2 parents 15ba2de + db81d59 commit 36edccb
Show file tree
Hide file tree
Showing 13 changed files with 490 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
INTERNALS_VERSION = v0.1.31
INTERNALS_VERSION = v0.1.32
INTERNALS_URL = https://github.com/AikidoSec/zen-internals/releases/download/$(INTERNALS_VERSION)
TARBALL = zen_internals.tgz
CHECKSUM_FILE = zen_internals.tgz.sha256sum
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Zen will autonomously protect your Node.js applications against:
* 🛡️ [Prototype pollution](./docs/prototype-pollution.md)
* 🛡️ [Path traversal attacks](https://owasp.org/www-community/attacks/Path_Traversal)
* 🛡️ [Server-side request forgery (SSRF)](./docs/ssrf.md)
* 🛡️ JS injection

Zen operates autonomously on the same server as your Node.js app to:

Expand Down
5 changes: 4 additions & 1 deletion library/agent/Attack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ export type Kind =
| "sql_injection"
| "shell_injection"
| "path_traversal"
| "ssrf";
| "ssrf"
| "js_injection";

export function attackKindHumanName(kind: Kind) {
switch (kind) {
Expand All @@ -17,5 +18,7 @@ export function attackKindHumanName(kind: Kind) {
return "a path traversal attack";
case "ssrf":
return "a server-side request forgery";
case "js_injection":
return "a JavaScript injection";
}
}
4 changes: 4 additions & 0 deletions library/agent/protect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import { Postgresjs } from "../sinks/Postgresjs";
import { Fastify } from "../sources/Fastify";
import { Koa } from "../sources/Koa";
import { ClickHouse } from "../sinks/ClickHouse";
import { Eval } from "../sinks/Eval";
import { Function } from "../sinks/Function";

function getLogger(): Logger {
if (isDebugging()) {
Expand Down Expand Up @@ -136,6 +138,8 @@ export function getWrappers() {
new Fastify(),
new Koa(),
new ClickHouse(),
new Eval(),
new Function(),
];
}

Expand Down
75 changes: 75 additions & 0 deletions library/sinks/Eval.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import * as t from "tap";
import { runWithContext, type Context } from "../agent/Context";
import { createTestAgent } from "../helpers/createTestAgent";
import { Eval } from "./Eval";

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 Eval", async (t) => {
const agent = createTestAgent();
agent.start([new Eval()]);

t.same(eval("1 + 1"), 2);
t.same(eval("1 + 1; console.log('hello')"), undefined);
t.same(eval("const x = 1 + 1; x"), 2);

runWithContext(dangerousContext, () => {
t.same(eval("1 + 1"), 2);
t.same(eval("const x = 1 + 1; x"), 2);

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

const error2 = t.throws(() =>
eval("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: eval(...) originating from body.calc"
);
}
});

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);
});
});
30 changes: 30 additions & 0 deletions library/sinks/Eval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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 Eval implements Wrapper {
private inspectEval(args: any[]) {
const context = getContext();

if (!context) {
return undefined;
}

if (args.length === 1 && typeof args[0] === "string") {
return checkContextForJsInjection({
js: args[0],
operation: "eval",
context,
});
}

return undefined;
}

wrap(hooks: Hooks) {
hooks.addGlobal("eval", {
inspectArgs: this.inspectEval,
});
}
}
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,
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as t from "tap";
import { checkContextForJsInjection } from "./checkContextForJsInjection";

t.test("it returns correct path", async () => {
t.same(
checkContextForJsInjection({
js: "const x = 1 + 1; fetch();",
operation: "eval",
context: {
cookies: {},
headers: {},
remoteAddress: "ip",
method: "POST",
url: "url",
query: {},
body: {
calc: "1 + 1; fetch()",
},
source: "express",
route: "/",
routeParams: {},
},
}),
{
operation: "eval",
kind: "js_injection",
source: "body",
pathToPayload: ".calc",
metadata: {
js: "const x = 1 + 1; fetch();",
},
payload: "1 + 1; fetch()",
}
);
});
41 changes: 41 additions & 0 deletions library/vulnerabilities/js-injection/checkContextForJsInjection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Context } from "../../agent/Context";
import { InterceptorResult } from "../../agent/hooks/InterceptorResult";
import { SOURCES } from "../../agent/Source";
import { extractStringsFromUserInputCached } from "../../helpers/extractStringsFromUserInputCached";
import { detectJsInjection } from "./detectJsInjection";

/**
* This function goes over all the different input types in the context and checks
* if it's a possible JS Injection, if so the function returns an InterceptorResult
*/
export function checkContextForJsInjection({
js,
operation,
context,
}: {
js: string;
operation: string;
context: Context;
}): InterceptorResult {
for (const source of SOURCES) {
const userInput = extractStringsFromUserInputCached(context, source);
if (!userInput) {
continue;
}

for (const [str, path] of userInput.entries()) {
if (detectJsInjection(js, str)) {
return {
operation: operation,
kind: "js_injection",
source: source,
pathToPayload: path,
metadata: {
js: js,
},
payload: str,
};
}
}
}
}
Loading

0 comments on commit 36edccb

Please sign in to comment.