From 7e2f2b658997b0aaf6fddea60f40ed5a28362f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Mon, 25 Nov 2024 13:48:02 +0100 Subject: [PATCH] Add e2e tests and improve unit tests --- Makefile | 4 + README.md | 1 + end2end/tests/hono-prisma.test.js | 127 ++++++++++++++ library/agent/hooks/wrapNewInstance.test.ts | 70 ++++++++ library/sinks/Prisma.test.ts | 13 ++ library/sinks/Prisma.ts | 10 +- sample-apps/hono-prisma/.gitignore | 3 + sample-apps/hono-prisma/README.md | 3 + sample-apps/hono-prisma/app.js | 49 ++++++ sample-apps/hono-prisma/package-lock.json | 155 ++++++++++++++++++ sample-apps/hono-prisma/package.json | 12 ++ .../20241122094425_init/migration.sql | 19 +++ .../prisma/migrations/migration_lock.toml | 3 + sample-apps/hono-prisma/prisma/schema.prisma | 27 +++ 14 files changed, 487 insertions(+), 9 deletions(-) create mode 100644 end2end/tests/hono-prisma.test.js create mode 100644 sample-apps/hono-prisma/.gitignore create mode 100644 sample-apps/hono-prisma/README.md create mode 100644 sample-apps/hono-prisma/app.js create mode 100644 sample-apps/hono-prisma/package-lock.json create mode 100644 sample-apps/hono-prisma/package.json create mode 100644 sample-apps/hono-prisma/prisma/migrations/20241122094425_init/migration.sql create mode 100644 sample-apps/hono-prisma/prisma/migrations/migration_lock.toml create mode 100644 sample-apps/hono-prisma/prisma/schema.prisma diff --git a/Makefile b/Makefile index 69203c5c9..87116b624 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 711fdc68b..ae47cd0a4 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/end2end/tests/hono-prisma.test.js b/end2end/tests/hono-prisma.test.js new file mode 100644 index 000000000..94ff9a367 --- /dev/null +++ b/end2end/tests/hono-prisma.test.js @@ -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(); + }); +}); diff --git a/library/agent/hooks/wrapNewInstance.test.ts b/library/agent/hooks/wrapNewInstance.test.ts index fa22a7201..ae93519af 100644 --- a/library/agent/hooks/wrapNewInstance.test.ts +++ b/library/agent/hooks/wrapNewInstance.test.ts @@ -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", + ]); +}); diff --git a/library/sinks/Prisma.test.ts b/library/sinks/Prisma.test.ts index 5166ed5d1..f47bcc153 100644 --- a/library/sinks/Prisma.test.ts +++ b/library/sinks/Prisma.test.ts @@ -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(); diff --git a/library/sinks/Prisma.ts b/library/sinks/Prisma.ts index 9e3d9ea0e..64b2cdcf6 100644 --- a/library/sinks/Prisma.ts +++ b/library/sinks/Prisma.ts @@ -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; } private inspectNoSQLFilter( - db: string, collection: string, request: Context, filter: unknown, @@ -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), diff --git a/sample-apps/hono-prisma/.gitignore b/sample-apps/hono-prisma/.gitignore new file mode 100644 index 000000000..0e3218f21 --- /dev/null +++ b/sample-apps/hono-prisma/.gitignore @@ -0,0 +1,3 @@ +node_modules +*.db +*.db-journal \ No newline at end of file diff --git a/sample-apps/hono-prisma/README.md b/sample-apps/hono-prisma/README.md new file mode 100644 index 000000000..b2507e190 --- /dev/null +++ b/sample-apps/hono-prisma/README.md @@ -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). diff --git a/sample-apps/hono-prisma/app.js b/sample-apps/hono-prisma/app.js new file mode 100644 index 000000000..8a718ce0c --- /dev/null +++ b/sample-apps/hono-prisma/app.js @@ -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); +}); diff --git a/sample-apps/hono-prisma/package-lock.json b/sample-apps/hono-prisma/package-lock.json new file mode 100644 index 000000000..04e623fab --- /dev/null +++ b/sample-apps/hono-prisma/package-lock.json @@ -0,0 +1,155 @@ +{ + "name": "hono-prisma", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hono-prisma", + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "@hono/node-server": "^1.11.2", + "@prisma/client": "^5.22.0", + "hono": "^4.6.8" + }, + "devDependencies": { + "prisma": "^5.22.0" + } + }, + "../../build": { + "name": "@aikidosec/firewall", + "version": "0.0.0", + "license": "AGPL-3.0-or-later", + "engines": { + "node": ">=16" + } + }, + "node_modules/@aikidosec/firewall": { + "resolved": "../../build", + "link": true + }, + "node_modules/@hono/node-server": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.13.7.tgz", + "integrity": "sha512-kTfUMsoloVKtRA2fLiGSd9qBddmru9KadNyhJCwgKBxTiNkaAJEwkVN9KV/rS4HtmmNRtUh6P+YpmjRMl0d9vQ==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hono": { + "version": "4.6.8", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.6.8.tgz", + "integrity": "sha512-f+2Ec9JAzabT61pglDiLJcF/DjiSefZkjCn9bzm1cYLGkD5ExJ3Jnv93ax9h0bn7UPLHF81KktoyjdQfWI2n1Q==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + } + } +} diff --git a/sample-apps/hono-prisma/package.json b/sample-apps/hono-prisma/package.json new file mode 100644 index 000000000..97c2f13df --- /dev/null +++ b/sample-apps/hono-prisma/package.json @@ -0,0 +1,12 @@ +{ + "name": "hono-prisma", + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "@hono/node-server": "^1.11.2", + "@prisma/client": "^5.22.0", + "hono": "^4.6.8" + }, + "devDependencies": { + "prisma": "^5.22.0" + } +} diff --git a/sample-apps/hono-prisma/prisma/migrations/20241122094425_init/migration.sql b/sample-apps/hono-prisma/prisma/migrations/20241122094425_init/migration.sql new file mode 100644 index 000000000..16407fbf7 --- /dev/null +++ b/sample-apps/hono-prisma/prisma/migrations/20241122094425_init/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "email" TEXT NOT NULL, + "name" TEXT +); + +-- CreateTable +CREATE TABLE "Post" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "title" TEXT NOT NULL, + "content" TEXT, + "published" BOOLEAN NOT NULL DEFAULT false, + "authorId" INTEGER NOT NULL, + CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/sample-apps/hono-prisma/prisma/migrations/migration_lock.toml b/sample-apps/hono-prisma/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..e5e5c4705 --- /dev/null +++ b/sample-apps/hono-prisma/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/sample-apps/hono-prisma/prisma/schema.prisma b/sample-apps/hono-prisma/prisma/schema.prisma new file mode 100644 index 000000000..59588b7a5 --- /dev/null +++ b/sample-apps/hono-prisma/prisma/schema.prisma @@ -0,0 +1,27 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + content String? + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id]) + authorId Int +} \ No newline at end of file