Skip to content

Commit

Permalink
Merge pull request #137 from AikidoSec/AIK-2617
Browse files Browse the repository at this point in the history
Aik 2617
  • Loading branch information
willem-delbare authored Apr 3, 2024
2 parents a1f9376 + 479cc3d commit eca0ac2
Show file tree
Hide file tree
Showing 18 changed files with 641 additions and 1 deletion.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ generated_docs
# Credentials
.env
credentials.json

# Test files
library/test.txt
library/test2.txt
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ express-mysql2:
express-mariadb:
cd sample-apps/express-mariadb && AIKIDO_DEBUG=true AIKIDO_BLOCKING=true node app.js

.PHONY: express-path-traversal
express-path-traversal:
cd sample-apps/express-path-traversal && AIKIDO_DEBUG=true AIKIDO_BLOCKING=true node app.js

.PHONY: lambda-mongodb-nosql-injection
lambda-mongodb-nosql-injection:
cd sample-apps/lambda-mongodb && npx serverless invoke local -e AIKIDO_BLOCKING=true --function login --path payloads/nosql-injection-request.json
Expand Down
116 changes: 116 additions & 0 deletions end2end/tests/express-path-traversal.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
const t = require("tap");
const { spawn } = require("child_process");
const { resolve } = require("path");
const timeout = require("../timeout");

const pathToApp = resolve(
__dirname,
"../../sample-apps/express-path-traversal",
"app.js"
);

t.test("it blocks in blocking mode", (t) => {
const server = spawn(`node`, [pathToApp, "4000"], {
env: { ...process.env, AIKIDO_DEBUG: "true", AIKIDO_BLOCKING: "true" },
});

server.on("close", () => {
t.end();
});

server.on("error", (err) => {
t.fail(err.message);
});

let stdout = "";
server.stdout.on("data", (data) => {
stdout += data.toString();
});

let stderr = "";
server.stderr.on("data", (data) => {
stderr += data.toString();
});

// Wait for the server to start
timeout(2000)
.then(() => {
return Promise.all([
fetch(
"http://localhost:4000/?content=blablabla&filename=/../TestDoc.txt",
{
signal: AbortSignal.timeout(5000),
}
),
fetch(
"http://localhost:4000/?content=blablabla&filename=/TestDoc.txt",
{
signal: AbortSignal.timeout(5000),
}
),
]);
})
.then(([pathTraversal, normalSearch]) => {
t.equal(pathTraversal.status, 500);
t.equal(normalSearch.status, 200);
t.match(stdout, /Starting agent/);
t.match(stderr, /Aikido runtime has blocked a Path traversal/);
})
.catch((error) => {
t.fail(error.message);
})
.finally(() => {
server.kill();
});
});

t.test("it does not block in dry mode", (t) => {
const server = spawn(`node`, [pathToApp, "4001"], {
env: { ...process.env, AIKIDO_DEBUG: "true" },
});

server.on("close", () => {
t.end();
});

let stdout = "";
server.stdout.on("data", (data) => {
stdout += data.toString();
});

let stderr = "";
server.stderr.on("data", (data) => {
stderr += data.toString();
});

// Wait for the server to start
timeout(2000)
.then(() =>
Promise.all([
fetch(
"http://localhost:4001/?content=blablabla&filename=/../TestDoc.txt",
{
signal: AbortSignal.timeout(5000),
}
),
fetch(
"http://localhost:4001/?content=blablabla&filename=/TestDoc.txt",
{
signal: AbortSignal.timeout(5000),
}
),
])
)
.then(([pathTraversal, normalSearch]) => {
t.equal(pathTraversal.status, 200);
t.equal(normalSearch.status, 200);
t.match(stdout, /Starting agent/);
t.notMatch(stderr, /Aikido runtime has blocked a Path traversal/);
})
.catch((error) => {
t.fail(error.message);
})
.finally(() => {
server.kill();
});
});
8 changes: 7 additions & 1 deletion library/agent/Attack.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export type Kind = "nosql_injection" | "sql_injection" | "shell_injection";
export type Kind =
| "nosql_injection"
| "sql_injection"
| "shell_injection"
| "path_traversal";

export function attackKindHumanName(kind: Kind) {
switch (kind) {
Expand All @@ -8,5 +12,7 @@ export function attackKindHumanName(kind: Kind) {
return "SQL injection";
case "shell_injection":
return "Shell injection";
case "path_traversal":
return "Path traversal";
}
}
2 changes: 2 additions & 0 deletions library/agent/protect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Token } from "./api/Token";
import { Logger } from "./logger/Logger";
import { LoggerConsole } from "./logger/LoggerConsole";
import { LoggerNoop } from "./logger/LoggerNoop";
import { FileSystem } from "../sinks/FileSystem";

function isDebugging() {
return (
Expand Down Expand Up @@ -107,6 +108,7 @@ function getWrappers() {
new PubSub(),
new FunctionsFramework(),
new ChildProcess(),
new FileSystem(),
];
}

Expand Down
126 changes: 126 additions & 0 deletions library/sinks/FileSystem.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import * as t from "tap";
import { Agent } from "../agent/Agent";
import { APIForTesting } from "../agent/api/APIForTesting";
import { Context, runWithContext } from "../agent/Context";
import { LoggerNoop } from "../agent/logger/LoggerNoop";
import { FileSystem } from "./FileSystem";

const unsafeContext: Context = {
remoteAddress: "::1",
method: "POST",
url: "http://localhost:4000",
query: {},
headers: {},
body: {
file: {
matches: "../test.txt",
},
},
cookies: {},
source: "express",
};

function throws(fn: () => void, wanted: string | RegExp) {
const error = t.throws(fn);
if (error instanceof Error) {
t.match(error.message, wanted);
}
}

t.test("it works", async (t) => {
const agent = new Agent(
true,
new LoggerNoop(),
new APIForTesting(),
undefined,
"lambda"
);

agent.start([new FileSystem()]);

const { writeFile, writeFileSync, rename } = require("fs");
const { writeFile: writeFilePromise } = require("fs/promises");

const runCommandsWithInvalidArgs = () => {
throws(() => writeFile(), /Received undefined/);
throws(() => writeFileSync(), /Received undefined/);
};

runCommandsWithInvalidArgs();

runWithContext(unsafeContext, () => {
runCommandsWithInvalidArgs();
});

const runSafeCommands = async () => {
writeFile(
"./test.txt",
"some file content to test with",
{ encoding: "utf-8" },
(err) => {}
);
writeFileSync("./test.txt", "some other file content to test with", {
encoding: "utf-8",
});
await writeFilePromise(
"./test.txt",
"some other file content to test with",
{ encoding: "utf-8" }
);
rename("./test.txt", "./test2.txt", (err) => {});
};

await runSafeCommands();

await runWithContext(unsafeContext, async () => {
await runSafeCommands();
});

await runWithContext(unsafeContext, async () => {
throws(
() =>
writeFile(
"../../test.txt",
"some file content to test with",
{ encoding: "utf-8" },
(err) => {}
),
"Aikido runtime has blocked a Path traversal: fs.writeFile(...) originating from body.file.matches"
);

throws(
() =>
writeFileSync(
"../../test.txt",
"some other file content to test with",
{ encoding: "utf-8" }
),
"Aikido runtime has blocked a Path traversal: fs.writeFileSync(...) originating from body.file.matches"
);

const error = await t.rejects(() =>
writeFilePromise(
"../../test.txt",
"some other file content to test with",
{ encoding: "utf-8" }
)
);

if (error instanceof Error) {
t.match(
error.message,
"Aikido runtime has blocked a Path traversal: fs.writeFile(...) originating from body.file.matches"
);
}

throws(
() => rename("../../test.txt", "./test2.txt", (err) => {}),
"Aikido runtime has blocked a Path traversal: fs.rename(...) originating from body.file.matches"
);

throws(
() => rename("./test.txt", "../../test.txt", (err) => {}),
"Aikido runtime has blocked a Path traversal: fs.rename(...) originating from body.file.matches"
);
});
});
Loading

0 comments on commit eca0ac2

Please sign in to comment.