Skip to content

Commit

Permalink
Detect JS injections using eval(...)
Browse files Browse the repository at this point in the history
  • Loading branch information
timokoessler committed Dec 13, 2024
1 parent 7c3c217 commit 3f3ff97
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 1 deletion.
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";
}
}
2 changes: 2 additions & 0 deletions library/agent/protect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ 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";

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

Expand Down
68 changes: 68 additions & 0 deletions library/sinks/Eval.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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: {
cakc: "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"
);
}
});
});
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,
});
}
}
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,
};
}
}
}
}
15 changes: 15 additions & 0 deletions library/vulnerabilities/js-injection/detectJsInjection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { shouldReturnEarly } from "./shouldReturnEarly";
// eslint-disable-next-line camelcase
import { wasm_detect_js_injection } from "../../internals/zen_internals";

export function detectJsInjection(query: string, userInput: string) {
const queryLowercase = query.toLowerCase();
const userInputLowercase = userInput.toLowerCase();

if (shouldReturnEarly(queryLowercase, userInputLowercase)) {
return false;
}

// The source type is currently hardcoded to 0 (CJS & ESM)
return wasm_detect_js_injection(queryLowercase, userInputLowercase, 0);
}
26 changes: 26 additions & 0 deletions library/vulnerabilities/js-injection/shouldReturnEarly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export function shouldReturnEarly(code: string, userInput: string) {
// User input too small or larger than query
if (userInput.length <= 1 || code.length < userInput.length) {
return true;
}

// User input not in query
if (!code.includes(userInput)) {
return true;
}

// User input is alphanumerical (with underscores allowed)
if (userInput.match(/^[a-z0-9_]+$/i)) {
return true;
}

// Check if user input is a valid comma-separated list of numbers
const cleanedInputForList = userInput.replace(/ /g, "").replace(/,/g, "");

if (/^\d+$/.test(cleanedInputForList)) {
return true;
}

// Return false if none of the conditions are met
return false;
}

0 comments on commit 3f3ff97

Please sign in to comment.