Skip to content

Commit

Permalink
Add e2e tests and improve unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
timokoessler committed Nov 25, 2024
1 parent 8d1793a commit 7e2f2b6
Show file tree
Hide file tree
Showing 14 changed files with 487 additions and 9 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ koa-sqlite3:
fastify-clickhouse:
cd sample-apps/fastify-clickhouse && AIKIDO_DEBUG=true AIKIDO_BLOCKING=true node app.js

.PHONY: hono-prisma
hono-prisma:
cd sample-apps/hono-prisma && AIKIDO_DEBUG=true AIKIDO_BLOCK=true node app.js

.PHONY: install
install:
mkdir -p build
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Zen for Node.js 16+ is compatible with:
*[`better-sqlite3`](https://www.npmjs.com/package/better-sqlite3) 11.x, 10.x, 9.x and 8.x
*[`postgres`](https://www.npmjs.com/package/postgres) 3.x
*[`@clickhouse/client`](https://www.npmjs.com/package/@clickhouse/client) 1.x
*[`@prisma/client`](https://www.npmjs.com/package/@prisma/client) 5.x

### Cloud providers

Expand Down
127 changes: 127 additions & 0 deletions end2end/tests/hono-prisma.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
const t = require("tap");
const { spawn } = require("child_process");
const { resolve, join } = require("path");
const timeout = require("../timeout");
const { promisify } = require("util");
const { exec: execCb } = require("child_process");

const execAsync = promisify(execCb);

const appDir = resolve(__dirname, "../../sample-apps/hono-prisma");
const pathToApp = join(appDir, "app.js");

t.before(async (t) => {
// Generate prismajs client
const { stdout, stderr } = await execAsync(
"npx prisma migrate reset --force", // Generate prisma client, reset db and apply migrations
{
cwd: appDir,
}
);

if (stderr) {
t.fail(stderr);
}
});

t.test("it blocks in blocking mode", (t) => {
const server = spawn(`node`, [pathToApp, "4002"], {
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://127.0.0.1:4002/posts/Test" OR 1=1 -- C', {
method: "GET",
signal: AbortSignal.timeout(5000),
}),
fetch("http://127.0.0.1:4002/posts/Happy", {
method: "GET",
signal: AbortSignal.timeout(5000),
}),
]);
})
.then(([sqlInjection, normalAdd]) => {
t.equal(sqlInjection.status, 500);
t.equal(normalAdd.status, 200);
t.match(stdout, /Starting agent/);
t.match(stderr, /Zen has blocked an SQL injection/);
})
.catch((error) => {
t.fail(error.message);
})
.finally(() => {
server.kill();
});
});

t.test("it does not block in non-blocking mode", (t) => {
const server = spawn(`node`, [pathToApp, "4002"], {
env: { ...process.env, AIKIDO_DEBUG: "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://127.0.0.1:4002/posts/Test" OR 1=1 -- C', {
method: "GET",
signal: AbortSignal.timeout(5000),
}),
fetch("http://127.0.0.1:4002/posts/Happy", {
method: "GET",
signal: AbortSignal.timeout(5000),
}),
]);
})
.then(([sqlInjection, normalAdd]) => {
t.equal(sqlInjection.status, 200);
t.equal(normalAdd.status, 200);
t.match(stdout, /Starting agent/);
t.notMatch(stderr, /Zen has blocked an SQL injection/);
})
.catch((error) => {
t.fail(error.message);
})
.finally(() => {
server.kill();
});
});
70 changes: 70 additions & 0 deletions library/agent/hooks/wrapNewInstance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,73 @@ t.test("Can wrap default export", async (t) => {
// @ts-expect-error Test method is added by interceptor
t.same(instance.testMethod(), "aikido");
});

t.test("Errors in interceptor are caught", async (t) => {
const exports = {
test: class Test {
constructor(private input: string) {}

getInput() {
return this.input;
}
},
};

logger.clear();

wrapNewInstance(exports, "test", { name: "test", type: "external" }, () => {
throw new Error("test error");
});

const instance = new exports.test("input");
t.same(instance.getInput(), "input");
t.same(logger.getMessages(), ["Failed to wrap method test in module test"]);
});

t.test("Return value from interceptor is returned", async (t) => {
const exports = {
test: class Test {
constructor(private input: string) {}

getInput() {
return this.input;
}
},
};

wrapNewInstance(exports, "test", { name: "test", type: "external" }, () => {
return { testMethod: () => "aikido" };
});

const instance = new exports.test("input");
t.same(typeof instance.getInput, "undefined");
// @ts-expect-error Test method is added by interceptor
t.same(instance.testMethod(), "aikido");
});

t.test("Logs error when wrapping default export", async (t) => {
let exports = class Test {
constructor(private input: string) {}

getInput() {
return this.input;
}
};

logger.clear();

exports = wrapNewInstance(
exports,
undefined,
{ name: "test", type: "external" },
() => {
throw new Error("test error");
}
) as any;

const instance = new exports("input");
t.same(instance.getInput(), "input");
t.same(logger.getMessages(), [
"Failed to wrap method default export in module test",
]);
});
13 changes: 13 additions & 0 deletions library/sinks/Prisma.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,19 @@ t.test("it works with sqlite", async (t) => {
);
}
}

try {
await client.$executeRawUnsafe();
t.fail("Should not be reached");
} catch (error) {
t.ok(error instanceof Error);
if (error instanceof Error) {
t.match(
error.message,
/Invalid `prisma\.\$executeRawUnsafe\(\)` invocation/
);
}
}
});

await client.$disconnect();
Expand Down
10 changes: 1 addition & 9 deletions library/sinks/Prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,20 +129,13 @@ export class Prisma implements Wrapper {
}

if (filter) {
return this.inspectNoSQLFilter(
model || "",
"",
context,
filter,
operation
);
return this.inspectNoSQLFilter(model ?? "", context, filter, operation);
}

return undefined;
}

Check warning on line 136 in library/sinks/Prisma.ts

View check run for this annotation

Codecov / codecov/patch

library/sinks/Prisma.ts#L135-L136

Added lines #L135 - L136 were not covered by tests

private inspectNoSQLFilter(
db: string,
collection: string,
request: Context,
filter: unknown,
Expand All @@ -157,7 +150,6 @@ export class Prisma implements Wrapper {
source: result.source,
pathToPayload: result.pathToPayload,
metadata: {
db: db,
collection: collection,
operation: operation,
filter: JSON.stringify(filter),
Expand Down
3 changes: 3 additions & 0 deletions sample-apps/hono-prisma/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
*.db
*.db-journal
3 changes: 3 additions & 0 deletions sample-apps/hono-prisma/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# hono-prisma

WARNING: This application contains security issues and should not be used in production (or taken as an example of how to write secure code).
49 changes: 49 additions & 0 deletions sample-apps/hono-prisma/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const Zen = require("@aikidosec/firewall");

const { PrismaClient } = require("@prisma/client");
const { serve } = require("@hono/node-server");
const { Hono } = require("hono");

function getPort() {
const port = parseInt(process.argv[2], 10) || 4000;

if (isNaN(port)) {
console.error("Invalid port");
process.exit(1);
}

return port;
}

async function main() {
const port = getPort();

const prisma = new PrismaClient();

const app = new Hono();

app.get("/", async (c) => {
return c.text("Hello, world!");
});

app.get("/posts/:title", async (c) => {
// Insecure, do not use in production
const posts = await prisma.$queryRawUnsafe(
'SELECT * FROM Post WHERE `title` = "' + c.req.param().title + '"'
);
return c.json(posts);
});

serve({
fetch: app.fetch,
port: port,
}).on("listening", () => {
console.log(`Server is running on port ${port}`);
});
}

main().catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
Loading

0 comments on commit 7e2f2b6

Please sign in to comment.