From 08638c16807dad8d4916b5485f418943ce4998cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Fri, 22 Nov 2024 13:01:31 +0100 Subject: [PATCH 01/17] Add initial prisma support --- library/agent/protect.ts | 2 + library/package-lock.json | 91 ++++++++++++++++++ library/package.json | 2 + library/sinks/Prisma.test.ts | 75 +++++++++++++++ library/sinks/Prisma.ts | 94 +++++++++++++++++++ library/sinks/fixtures/.gitignore | 3 + .../20241122110725_init/migration.sql | 19 ++++ .../prisma/migrations/migration_lock.toml | 3 + library/sinks/fixtures/prisma/schema.prisma | 27 ++++++ .../dialects/SQLDialectGeneric.ts | 7 ++ 10 files changed, 323 insertions(+) create mode 100644 library/sinks/Prisma.test.ts create mode 100644 library/sinks/Prisma.ts create mode 100644 library/sinks/fixtures/.gitignore create mode 100644 library/sinks/fixtures/prisma/migrations/20241122110725_init/migration.sql create mode 100644 library/sinks/fixtures/prisma/migrations/migration_lock.toml create mode 100644 library/sinks/fixtures/prisma/schema.prisma create mode 100644 library/vulnerabilities/sql-injection/dialects/SQLDialectGeneric.ts diff --git a/library/agent/protect.ts b/library/agent/protect.ts index 76fc331f4..f1909a308 100644 --- a/library/agent/protect.ts +++ b/library/agent/protect.ts @@ -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 { Prisma } from "../sinks/Prisma"; function getLogger(): Logger { if (isDebugging()) { @@ -136,6 +137,7 @@ function getWrappers() { new Fastify(), new Koa(), new ClickHouse(), + new Prisma(), ]; } diff --git a/library/package-lock.json b/library/package-lock.json index caf0df0d6..f703744c2 100644 --- a/library/package-lock.json +++ b/library/package-lock.json @@ -18,6 +18,7 @@ "@hono/node-server": "^1.12.2", "@koa/bodyparser": "^5.1.1", "@koa/router": "^13.0.0", + "@prisma/client": "^5.22.0", "@sinonjs/fake-timers": "^11.2.2", "@types/aws-lambda": "^8.10.131", "@types/cookie-parser": "^1.4.6", @@ -65,6 +66,7 @@ "pg": "^8.11.3", "postgres": "^3.4.4", "prettier": "^3.2.4", + "prisma": "^5.22.0", "shell-quote": "^1.8.1", "shelljs": "^0.8.5", "sqlite3": "^5.1.7", @@ -2260,6 +2262,75 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "dev": true, + "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==", + "dev": 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==", + "dev": 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==", + "dev": 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==", + "dev": 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==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -11553,6 +11624,26 @@ "node": ">=6.0.0" } }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "dev": 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" + } + }, "node_modules/prismjs": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", diff --git a/library/package.json b/library/package.json index d9a9e937e..9698047e7 100644 --- a/library/package.json +++ b/library/package.json @@ -42,6 +42,7 @@ "@hono/node-server": "^1.12.2", "@koa/bodyparser": "^5.1.1", "@koa/router": "^13.0.0", + "@prisma/client": "^5.22.0", "@sinonjs/fake-timers": "^11.2.2", "@types/aws-lambda": "^8.10.131", "@types/cookie-parser": "^1.4.6", @@ -89,6 +90,7 @@ "pg": "^8.11.3", "postgres": "^3.4.4", "prettier": "^3.2.4", + "prisma": "^5.22.0", "shell-quote": "^1.8.1", "shelljs": "^0.8.5", "sqlite3": "^5.1.7", diff --git a/library/sinks/Prisma.test.ts b/library/sinks/Prisma.test.ts new file mode 100644 index 000000000..9f1200037 --- /dev/null +++ b/library/sinks/Prisma.test.ts @@ -0,0 +1,75 @@ +import * as t from "tap"; +import { runWithContext, type Context } from "../agent/Context"; +import { Prisma } from "./Prisma"; +import { createTestAgent } from "../helpers/createTestAgent"; +import { promisify } from "util"; +import { exec as execCb } from "child_process"; +import path = require("path"); + +const execAsync = promisify(execCb); + +const context: Context = { + remoteAddress: "::1", + method: "POST", + url: "http://localhost:4000", + query: {}, + headers: {}, + body: { + myTitle: `-- should be blocked`, + }, + cookies: {}, + routeParams: {}, + source: "express", + route: "/posts/:id", +}; + +t.test("it inspects query method calls and blocks if needed", async (t) => { + const agent = createTestAgent(); + agent.start([new Prisma()]); + + // Generate prismajs client + const { stdout, stderr } = await execAsync( + "npx prisma migrate reset --force", // Generate prisma client, reset db and apply migrations + { + cwd: path.join(__dirname, "fixtures"), + } + ); + + if (stderr) { + t.fail(stderr); + } + + const { PrismaClient } = require("@prisma/client"); + + const client = new PrismaClient(); + + const user = await client.user.create({ + data: { + name: "Alice", + email: "alice@example.com", + }, + }); + + t.same(await client.$queryRawUnsafe("SELECT * FROM USER"), [ + { + id: user.id, + name: "Alice", + email: "alice@example.com", + }, + ]); + + await runWithContext(context, async () => { + try { + await client.$queryRawUnsafe("SELECT * FROM USER -- should be blocked"); + t.fail("Query should be blocked"); + } catch (error) { + t.ok(error instanceof Error); + if (error instanceof Error) { + t.same( + error.message, + "Zen has blocked an SQL injection: prisma.$queryRawUnsafe(...) originating from body.myTitle" + ); + } + } + }); +}); diff --git a/library/sinks/Prisma.ts b/library/sinks/Prisma.ts new file mode 100644 index 000000000..6f4e8013e --- /dev/null +++ b/library/sinks/Prisma.ts @@ -0,0 +1,94 @@ +import type { Hooks } from "../agent/hooks/Hooks"; +import { Wrapper } from "../agent/Wrapper"; +import { wrapExport } from "../agent/hooks/wrapExport"; +import { wrapNewInstance } from "../agent/hooks/wrapNewInstance"; +import { SQLDialect } from "../vulnerabilities/sql-injection/dialects/SQLDialect"; +import { SQLDialectMySQL } from "../vulnerabilities/sql-injection/dialects/SQLDialectMySQL"; +import { SQLDialectGeneric } from "../vulnerabilities/sql-injection/dialects/SQLDialectGeneric"; +import { SQLDialectPostgres } from "../vulnerabilities/sql-injection/dialects/SQLDialectPostgres"; +import { SQLDialectSQLite } from "../vulnerabilities/sql-injection/dialects/SQLDialectSQLite"; +import type { InterceptorResult } from "../agent/hooks/InterceptorResult"; +import { checkContextForSqlInjection } from "../vulnerabilities/sql-injection/checkContextForSqlInjection"; +import { getContext } from "../agent/Context"; + +export class Prisma implements Wrapper { + private rawSQLMethodsToWrap = ["$queryRawUnsafe", "$executeRawUnsafe"]; + + private dialect: SQLDialect = new SQLDialectGeneric(); + + // Try to detect the SQL dialect used by the Prisma client, so we can use the correct SQL dialect for the SQL injection detection. + private detectSQLDialect(clientInstance: any) { + // https://github.com/prisma/prisma/blob/559988a47e50b4d4655dc45b11ceb9b5c73ef053/packages/generator-helper/src/types.ts#L75 + if ( + !clientInstance || + typeof clientInstance !== "object" || + !("_accelerateEngineConfig" in clientInstance) || + !clientInstance._accelerateEngineConfig || + typeof clientInstance._accelerateEngineConfig !== "object" || + !("activeProvider" in clientInstance._accelerateEngineConfig) || + typeof clientInstance._accelerateEngineConfig.activeProvider !== "string" + ) { + return; + } + + switch (clientInstance._accelerateEngineConfig.activeProvider) { + case "mysql": + this.dialect = new SQLDialectMySQL(); + break; + case "postgresql": + case "postgres": + this.dialect = new SQLDialectPostgres(); + break; + case "sqlite": + this.dialect = new SQLDialectSQLite(); + break; + default: + // Already set to generic + break; + } + } + + private inspectQuery(args: unknown[], operation: string): InterceptorResult { + const context = getContext(); + + if (!context) { + return undefined; + } + + if (args.length > 0 && typeof args[0] === "string" && args[0].length > 0) { + const sql: string = args[0]; + + return checkContextForSqlInjection({ + sql: sql, + context: context, + operation: `prisma.${operation}`, + dialect: this.dialect, + }); + } + + return undefined; + } + + wrap(hooks: Hooks) { + hooks + .addPackage("@prisma/client") + .withVersion("^5.0.0") + .onRequire((exports, pkgInfo) => { + wrapNewInstance(exports, "PrismaClient", pkgInfo, (instance) => { + this.detectSQLDialect(instance); + + for (const method of this.rawSQLMethodsToWrap) { + if (typeof instance[method] === "function") { + wrapExport(instance, method, pkgInfo, { + inspectArgs: (args) => { + return this.inspectQuery(args, method); + }, + }); + } + } + + // Todo support mongodb methods + }); + }); + } +} diff --git a/library/sinks/fixtures/.gitignore b/library/sinks/fixtures/.gitignore new file mode 100644 index 000000000..0e3218f21 --- /dev/null +++ b/library/sinks/fixtures/.gitignore @@ -0,0 +1,3 @@ +node_modules +*.db +*.db-journal \ No newline at end of file diff --git a/library/sinks/fixtures/prisma/migrations/20241122110725_init/migration.sql b/library/sinks/fixtures/prisma/migrations/20241122110725_init/migration.sql new file mode 100644 index 000000000..16407fbf7 --- /dev/null +++ b/library/sinks/fixtures/prisma/migrations/20241122110725_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/library/sinks/fixtures/prisma/migrations/migration_lock.toml b/library/sinks/fixtures/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..e5e5c4705 --- /dev/null +++ b/library/sinks/fixtures/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/library/sinks/fixtures/prisma/schema.prisma b/library/sinks/fixtures/prisma/schema.prisma new file mode 100644 index 000000000..59588b7a5 --- /dev/null +++ b/library/sinks/fixtures/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 diff --git a/library/vulnerabilities/sql-injection/dialects/SQLDialectGeneric.ts b/library/vulnerabilities/sql-injection/dialects/SQLDialectGeneric.ts new file mode 100644 index 000000000..9b24338b2 --- /dev/null +++ b/library/vulnerabilities/sql-injection/dialects/SQLDialectGeneric.ts @@ -0,0 +1,7 @@ +import { SQLDialect } from "./SQLDialect"; + +export class SQLDialectGeneric implements SQLDialect { + getWASMDialectInt(): number { + return 0; + } +} From 1d349b97ac26caea5877347821bfd047dbcc2de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Fri, 22 Nov 2024 13:20:24 +0100 Subject: [PATCH 02/17] Fix Prisma sqlite test --- .../{Prisma.test.ts => Prisma.sqlite.test.ts} | 25 ++++++++++++++++++- library/sinks/Prisma.ts | 7 ++++-- .../20241122110725_init/migration.sql | 0 .../migrations/migration_lock.toml | 0 .../prisma/{ => sqlite}/schema.prisma | 0 5 files changed, 29 insertions(+), 3 deletions(-) rename library/sinks/{Prisma.test.ts => Prisma.sqlite.test.ts} (72%) rename library/sinks/fixtures/prisma/{ => sqlite}/migrations/20241122110725_init/migration.sql (100%) rename library/sinks/fixtures/prisma/{ => sqlite}/migrations/migration_lock.toml (100%) rename library/sinks/fixtures/prisma/{ => sqlite}/schema.prisma (100%) diff --git a/library/sinks/Prisma.test.ts b/library/sinks/Prisma.sqlite.test.ts similarity index 72% rename from library/sinks/Prisma.test.ts rename to library/sinks/Prisma.sqlite.test.ts index 9f1200037..55bb50f9c 100644 --- a/library/sinks/Prisma.test.ts +++ b/library/sinks/Prisma.sqlite.test.ts @@ -23,6 +23,8 @@ const context: Context = { route: "/posts/:id", }; +process.env.DATABASE_URL = "file:./dev.db"; + t.test("it inspects query method calls and blocks if needed", async (t) => { const agent = createTestAgent(); agent.start([new Prisma()]); @@ -31,7 +33,7 @@ t.test("it inspects query method calls and blocks if needed", async (t) => { const { stdout, stderr } = await execAsync( "npx prisma migrate reset --force", // Generate prisma client, reset db and apply migrations { - cwd: path.join(__dirname, "fixtures"), + cwd: path.join(__dirname, "fixtures/prisma/sqlite"), } ); @@ -72,4 +74,25 @@ t.test("it inspects query method calls and blocks if needed", async (t) => { } } }); + + await client.$executeRawUnsafe("DELETE FROM USER WHERE id = 1"); + + await runWithContext(context, async () => { + try { + await client.$executeRawUnsafe( + "DELETE FROM USER WHERE id = 1 -- should be blocked" + ); + t.fail("Execution should be blocked"); + } catch (error) { + t.ok(error instanceof Error); + if (error instanceof Error) { + t.same( + error.message, + "Zen has blocked an SQL injection: prisma.$executeRawUnsafe(...) originating from body.myTitle" + ); + } + } + }); + + await client.$disconnect(); }); diff --git a/library/sinks/Prisma.ts b/library/sinks/Prisma.ts index 6f4e8013e..325d30949 100644 --- a/library/sinks/Prisma.ts +++ b/library/sinks/Prisma.ts @@ -48,7 +48,10 @@ export class Prisma implements Wrapper { } } - private inspectQuery(args: unknown[], operation: string): InterceptorResult { + private inspectSQLQuery( + args: unknown[], + operation: string + ): InterceptorResult { const context = getContext(); if (!context) { @@ -81,7 +84,7 @@ export class Prisma implements Wrapper { if (typeof instance[method] === "function") { wrapExport(instance, method, pkgInfo, { inspectArgs: (args) => { - return this.inspectQuery(args, method); + return this.inspectSQLQuery(args, method); }, }); } diff --git a/library/sinks/fixtures/prisma/migrations/20241122110725_init/migration.sql b/library/sinks/fixtures/prisma/sqlite/migrations/20241122110725_init/migration.sql similarity index 100% rename from library/sinks/fixtures/prisma/migrations/20241122110725_init/migration.sql rename to library/sinks/fixtures/prisma/sqlite/migrations/20241122110725_init/migration.sql diff --git a/library/sinks/fixtures/prisma/migrations/migration_lock.toml b/library/sinks/fixtures/prisma/sqlite/migrations/migration_lock.toml similarity index 100% rename from library/sinks/fixtures/prisma/migrations/migration_lock.toml rename to library/sinks/fixtures/prisma/sqlite/migrations/migration_lock.toml diff --git a/library/sinks/fixtures/prisma/schema.prisma b/library/sinks/fixtures/prisma/sqlite/schema.prisma similarity index 100% rename from library/sinks/fixtures/prisma/schema.prisma rename to library/sinks/fixtures/prisma/sqlite/schema.prisma From 33bb42ea8d0a050682be79158297821c7aeae03c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Fri, 22 Nov 2024 14:28:47 +0100 Subject: [PATCH 03/17] Add prisma postgres test --- library/sinks/Prisma.sqlite.test.ts | 98 -------- library/sinks/Prisma.test.ts | 215 ++++++++++++++++++ .../20241122131705_init/migration.sql | 25 ++ .../postgres/migrations/migration_lock.toml | 3 + .../fixtures/prisma/postgres/schema.prisma | 27 +++ 5 files changed, 270 insertions(+), 98 deletions(-) delete mode 100644 library/sinks/Prisma.sqlite.test.ts create mode 100644 library/sinks/Prisma.test.ts create mode 100644 library/sinks/fixtures/prisma/postgres/migrations/20241122131705_init/migration.sql create mode 100644 library/sinks/fixtures/prisma/postgres/migrations/migration_lock.toml create mode 100644 library/sinks/fixtures/prisma/postgres/schema.prisma diff --git a/library/sinks/Prisma.sqlite.test.ts b/library/sinks/Prisma.sqlite.test.ts deleted file mode 100644 index 55bb50f9c..000000000 --- a/library/sinks/Prisma.sqlite.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import * as t from "tap"; -import { runWithContext, type Context } from "../agent/Context"; -import { Prisma } from "./Prisma"; -import { createTestAgent } from "../helpers/createTestAgent"; -import { promisify } from "util"; -import { exec as execCb } from "child_process"; -import path = require("path"); - -const execAsync = promisify(execCb); - -const context: Context = { - remoteAddress: "::1", - method: "POST", - url: "http://localhost:4000", - query: {}, - headers: {}, - body: { - myTitle: `-- should be blocked`, - }, - cookies: {}, - routeParams: {}, - source: "express", - route: "/posts/:id", -}; - -process.env.DATABASE_URL = "file:./dev.db"; - -t.test("it inspects query method calls and blocks if needed", async (t) => { - const agent = createTestAgent(); - agent.start([new Prisma()]); - - // Generate prismajs client - const { stdout, stderr } = await execAsync( - "npx prisma migrate reset --force", // Generate prisma client, reset db and apply migrations - { - cwd: path.join(__dirname, "fixtures/prisma/sqlite"), - } - ); - - if (stderr) { - t.fail(stderr); - } - - const { PrismaClient } = require("@prisma/client"); - - const client = new PrismaClient(); - - const user = await client.user.create({ - data: { - name: "Alice", - email: "alice@example.com", - }, - }); - - t.same(await client.$queryRawUnsafe("SELECT * FROM USER"), [ - { - id: user.id, - name: "Alice", - email: "alice@example.com", - }, - ]); - - await runWithContext(context, async () => { - try { - await client.$queryRawUnsafe("SELECT * FROM USER -- should be blocked"); - t.fail("Query should be blocked"); - } catch (error) { - t.ok(error instanceof Error); - if (error instanceof Error) { - t.same( - error.message, - "Zen has blocked an SQL injection: prisma.$queryRawUnsafe(...) originating from body.myTitle" - ); - } - } - }); - - await client.$executeRawUnsafe("DELETE FROM USER WHERE id = 1"); - - await runWithContext(context, async () => { - try { - await client.$executeRawUnsafe( - "DELETE FROM USER WHERE id = 1 -- should be blocked" - ); - t.fail("Execution should be blocked"); - } catch (error) { - t.ok(error instanceof Error); - if (error instanceof Error) { - t.same( - error.message, - "Zen has blocked an SQL injection: prisma.$executeRawUnsafe(...) originating from body.myTitle" - ); - } - } - }); - - await client.$disconnect(); -}); diff --git a/library/sinks/Prisma.test.ts b/library/sinks/Prisma.test.ts new file mode 100644 index 000000000..0e898870c --- /dev/null +++ b/library/sinks/Prisma.test.ts @@ -0,0 +1,215 @@ +import * as t from "tap"; +import { runWithContext, type Context } from "../agent/Context"; +import { Prisma } from "./Prisma"; +import { createTestAgent } from "../helpers/createTestAgent"; +import { promisify } from "util"; +import { exec as execCb } from "child_process"; +import path = require("path"); + +const execAsync = promisify(execCb); + +const context: Context = { + remoteAddress: "::1", + method: "POST", + url: "http://localhost:4000", + query: {}, + headers: {}, + body: { + myTitle: `-- should be blocked`, + }, + cookies: {}, + routeParams: {}, + source: "express", + route: "/posts/:id", +}; + +t.test("it works with sqlite", async (t) => { + const agent = createTestAgent(); + agent.start([new Prisma()]); + + process.env.DATABASE_URL = "file:./dev.db"; + + // Generate prismajs client + const { stdout, stderr } = await execAsync( + "npx prisma migrate reset --force", // Generate prisma client, reset db and apply migrations + { + cwd: path.join(__dirname, "fixtures/prisma/sqlite"), + } + ); + + if (stderr) { + t.fail(stderr); + } + + const { PrismaClient } = require("@prisma/client"); + + const client = new PrismaClient(); + + await client.user.create({ + data: { + name: "Alice", + email: "alice@example.com", + }, + }); + + t.same(await client.$queryRawUnsafe("SELECT * FROM USER"), [ + { + id: 1, + name: "Alice", + email: "alice@example.com", + }, + ]); + + await runWithContext(context, async () => { + t.same(await client.$queryRawUnsafe("SELECT * FROM USER"), [ + { + id: 1, + name: "Alice", + email: "alice@example.com", + }, + ]); + + try { + await client.$queryRawUnsafe("SELECT * FROM USER -- should be blocked"); + t.fail("Query should be blocked"); + } catch (error) { + t.ok(error instanceof Error); + if (error instanceof Error) { + t.same( + error.message, + "Zen has blocked an SQL injection: prisma.$queryRawUnsafe(...) originating from body.myTitle" + ); + } + } + }); + + await client.$executeRawUnsafe("DELETE FROM USER WHERE id = 1"); + + await client.user.create({ + data: { + name: "Alice2", + email: "alice2@example.com", + }, + }); + + await runWithContext(context, async () => { + await client.$executeRawUnsafe("DELETE FROM USER WHERE id = 2"); + + try { + await client.$executeRawUnsafe("DELETE FROM USER WHERE id = 2"); + await client.$executeRawUnsafe( + "DELETE FROM USER WHERE id = 1 -- should be blocked" + ); + t.fail("Execution should be blocked"); + } catch (error) { + t.ok(error instanceof Error); + if (error instanceof Error) { + t.same( + error.message, + "Zen has blocked an SQL injection: prisma.$executeRawUnsafe(...) originating from body.myTitle" + ); + } + } + }); + + await client.$disconnect(); +}); + +t.test("it works with postgres", async (t) => { + const agent = createTestAgent(); + agent.start([new Prisma()]); + + process.env.DATABASE_URL = "postgres://root:password@127.0.0.1:27016/main_db"; + + // Generate prismajs client + const { stdout, stderr } = await execAsync( + "npx prisma migrate reset --force", // Generate prisma client, reset db and apply migrations + { + cwd: path.join(__dirname, "fixtures/prisma/postgres"), + } + ); + + if (stderr) { + t.fail(stderr); + } + + // Clear require cache + for (const key in require.cache) { + delete require.cache[key]; + } + + const { PrismaClient } = require("@prisma/client"); + + const client = new PrismaClient(); + + await client.appUser.create({ + data: { + name: "Alice", + email: "alice@example.com", + }, + }); + + t.same(await client.$queryRawUnsafe('SELECT * FROM "AppUser";'), [ + { + id: 1, + name: "Alice", + email: "alice@example.com", + }, + ]); + + await runWithContext(context, async () => { + t.same(await client.$queryRawUnsafe('SELECT * FROM "AppUser";'), [ + { + id: 1, + name: "Alice", + email: "alice@example.com", + }, + ]); + + try { + await client.$queryRawUnsafe( + 'SELECT * FROM "AppUser" -- should be blocked' + ); + t.fail("Query should be blocked"); + } catch (error) { + t.ok(error instanceof Error); + if (error instanceof Error) { + t.same( + error.message, + "Zen has blocked an SQL injection: prisma.$queryRawUnsafe(...) originating from body.myTitle" + ); + } + } + }); + + await client.$executeRawUnsafe('DELETE FROM "AppUser" WHERE id = 1'); + + await client.appUser.create({ + data: { + name: "Alice2", + email: "alice2@example.com", + }, + }); + + await runWithContext(context, async () => { + await client.$executeRawUnsafe('DELETE FROM "AppUser" WHERE id = 2'); + + try { + await client.$executeRawUnsafe('DELETE FROM "AppUser" WHERE id = 2'); + await client.$executeRawUnsafe( + 'DELETE FROM "AppUser" WHERE id = 1 -- should be blocked' + ); + t.fail("Execution should be blocked"); + } catch (error) { + t.ok(error instanceof Error); + if (error instanceof Error) { + t.same( + error.message, + "Zen has blocked an SQL injection: prisma.$executeRawUnsafe(...) originating from body.myTitle" + ); + } + } + }); + + await client.$disconnect(); +}); diff --git a/library/sinks/fixtures/prisma/postgres/migrations/20241122131705_init/migration.sql b/library/sinks/fixtures/prisma/postgres/migrations/20241122131705_init/migration.sql new file mode 100644 index 000000000..93777787e --- /dev/null +++ b/library/sinks/fixtures/prisma/postgres/migrations/20241122131705_init/migration.sql @@ -0,0 +1,25 @@ +-- CreateTable +CREATE TABLE "AppUser" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT, + + CONSTRAINT "AppUser_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Post" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT, + "published" BOOLEAN NOT NULL DEFAULT false, + "authorId" INTEGER NOT NULL, + + CONSTRAINT "Post_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "AppUser_email_key" ON "AppUser"("email"); + +-- AddForeignKey +ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "AppUser"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/library/sinks/fixtures/prisma/postgres/migrations/migration_lock.toml b/library/sinks/fixtures/prisma/postgres/migrations/migration_lock.toml new file mode 100644 index 000000000..fbffa92c2 --- /dev/null +++ b/library/sinks/fixtures/prisma/postgres/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 = "postgresql" \ No newline at end of file diff --git a/library/sinks/fixtures/prisma/postgres/schema.prisma b/library/sinks/fixtures/prisma/postgres/schema.prisma new file mode 100644 index 000000000..23051681e --- /dev/null +++ b/library/sinks/fixtures/prisma/postgres/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 = "postgresql" + url = env("DATABASE_URL") +} + +model AppUser { + 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 AppUser @relation(fields: [authorId], references: [id]) + authorId Int +} \ No newline at end of file From e411408b82a249a319ffc24e2d11b765fc394777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Fri, 22 Nov 2024 15:36:54 +0100 Subject: [PATCH 04/17] Add initial prisma mongodb test --- .github/workflows/unit-test.yml | 10 +++++ library/sinks/Prisma.test.ts | 42 ++++++++++++++++++- .../fixtures/prisma/mongodb/schema.prisma | 27 ++++++++++++ sample-apps/docker-compose.yml | 14 +++++++ 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 library/sinks/fixtures/prisma/mongodb/schema.prisma diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index e59645983..3ee27d140 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -51,6 +51,16 @@ jobs: "CLICKHOUSE_DEFAULT_ACCESS": "MANAGEMENT=1" ports: - "27019:8123" + mongodb-replica: + image: bitnami/mongodb:8.0 + env: + MONGODB_ADVERTISED_HOSTNAME: 127.0.0.1 + MONGODB_REPLICA_SET_MODE: primary + MONGODB_ROOT_USER: root + MONGODB_ROOT_PASSWORD: password + MONGODB_REPLICA_SET_KEY: replicasetkey123 + ports: + - "27020:27017" strategy: fail-fast: false matrix: diff --git a/library/sinks/Prisma.test.ts b/library/sinks/Prisma.test.ts index 0e898870c..122eb7441 100644 --- a/library/sinks/Prisma.test.ts +++ b/library/sinks/Prisma.test.ts @@ -4,7 +4,7 @@ import { Prisma } from "./Prisma"; import { createTestAgent } from "../helpers/createTestAgent"; import { promisify } from "util"; import { exec as execCb } from "child_process"; -import path = require("path"); +import * as path from "path"; const execAsync = promisify(execCb); @@ -213,3 +213,43 @@ t.test("it works with postgres", async (t) => { await client.$disconnect(); }); + +t.test("it works with mongodb", async (t) => { + const agent = createTestAgent(); + agent.start([new Prisma()]); + + process.env.DATABASE_URL = + "mongodb://root:password@127.0.0.1:27020/prisma?authSource=admin&directConnection=true"; + + // Generate prismajs client + const { stdout, stderr } = await execAsync( + "npx prisma generate", // Generate prisma client, reset db and apply migrations + { + cwd: path.join(__dirname, "fixtures/prisma/mongodb"), + } + ); + + if (stderr) { + t.fail(stderr); + } + + // Clear require cache + for (const key in require.cache) { + delete require.cache[key]; + } + + const { PrismaClient } = require("@prisma/client"); + + const client = new PrismaClient(); + + await client.user.create({ + data: { + name: "Alice", + email: "alice@example.com", + }, + }); + + await client.user.deleteMany(); + + await client.$disconnect(); +}); diff --git a/library/sinks/fixtures/prisma/mongodb/schema.prisma b/library/sinks/fixtures/prisma/mongodb/schema.prisma new file mode 100644 index 000000000..e73734a9a --- /dev/null +++ b/library/sinks/fixtures/prisma/mongodb/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 = "mongodb" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(auto()) @map("_id") @db.ObjectId + email String @unique + name String? + posts Post[] +} + +model Post { + id String @id @default(auto()) @map("_id") @db.ObjectId + slug String @unique + title String + body String + author User @relation(fields: [authorId], references: [id]) + authorId String @db.ObjectId +} \ No newline at end of file diff --git a/sample-apps/docker-compose.yml b/sample-apps/docker-compose.yml index db4b28191..7533b86e5 100644 --- a/sample-apps/docker-compose.yml +++ b/sample-apps/docker-compose.yml @@ -57,6 +57,18 @@ services: - "27019:8123" volumes: - clickhouse:/var/lib/clickhouse + mongodb-replica: + image: bitnami/mongodb:4.4 # Newer versions do not run on Apple Silicon + environment: + - MONGODB_ADVERTISED_HOSTNAME=127.0.0.1 + - MONGODB_REPLICA_SET_MODE=primary + - MONGODB_ROOT_USER=root + - MONGODB_ROOT_PASSWORD=password + - MONGODB_REPLICA_SET_KEY=replicasetkey123 + ports: + - "27020:27017" + volumes: + - "mongodb-replica:/bitnami/mongodb" volumes: mongodb: @@ -69,3 +81,5 @@ volumes: driver: local clickhouse: driver: local + mongodb-replica: + driver: local From 83f6b0384f71697ebf1622655a2308aeb8235f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Mon, 25 Nov 2024 09:33:51 +0100 Subject: [PATCH 05/17] Fix tests in Node v23 --- scripts/run-tap.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/run-tap.js b/scripts/run-tap.js index cee877b1d..ed96e2f83 100644 --- a/scripts/run-tap.js +++ b/scripts/run-tap.js @@ -11,11 +11,16 @@ if (process.env.CI) { } // Enable the `--experimental-sqlite` flag for Node.js ^22.5.0 -if (major === 22 && minor >= 5) { +if ((major === 22 && minor >= 5) || major === 23) { args += " --node-arg=--experimental-sqlite --node-arg=--no-warnings"; } execSync(`tap ${args}`, { stdio: "inherit", - env: { ...process.env, AIKIDO_CI: "true" }, + env: { + ...process.env, + AIKIDO_CI: "true", + // In v23 some sub-dependencies are calling require on a esm module triggering an experimental warning + NODE_OPTIONS: major === 23 ? "--disable-warning=ExperimentalWarning" : "", + }, }); From 31059d916bfebdb852db4c4f8ad2e5309aab51a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Mon, 25 Nov 2024 11:14:31 +0100 Subject: [PATCH 06/17] Rewrite Prisma sink using client extensions --- library/agent/hooks/wrapExport.ts | 9 ++ library/agent/hooks/wrapNewInstance.ts | 14 ++- library/sinks/Prisma.ts | 144 +++++++++++++++++++------ 3 files changed, 133 insertions(+), 34 deletions(-) diff --git a/library/agent/hooks/wrapExport.ts b/library/agent/hooks/wrapExport.ts index 4c9624975..fd9fd8e1f 100644 --- a/library/agent/hooks/wrapExport.ts +++ b/library/agent/hooks/wrapExport.ts @@ -154,7 +154,16 @@ function inspectArgs( module: pkgInfo.name, }); } + onInspectionInterceptorResult(context, agent, result, pkgInfo, start); +} +export function onInspectionInterceptorResult( + context: ReturnType, + agent: Agent, + result: InterceptorResult, + pkgInfo: WrapPackageInfo, + start: number +) { const end = performance.now(); agent.getInspectionStatistics().onInspectedCall({ sink: pkgInfo.name, diff --git a/library/agent/hooks/wrapNewInstance.ts b/library/agent/hooks/wrapNewInstance.ts index 3923de4c8..c916d406c 100644 --- a/library/agent/hooks/wrapNewInstance.ts +++ b/library/agent/hooks/wrapNewInstance.ts @@ -9,7 +9,7 @@ export function wrapNewInstance( subject: unknown, className: string | undefined, pkgInfo: WrapPackageInfo, - interceptor: (exports: any) => void + interceptor: (exports: any) => void | unknown ) { const agent = getInstance(); if (!agent) { @@ -28,7 +28,17 @@ export function wrapNewInstance( // @ts-expect-error It's a constructor const newInstance = new original(...args); - interceptor(newInstance); + try { + const returnVal = interceptor(newInstance); + if (returnVal) { + return returnVal; + } + } catch (error) { + agent.onFailedToWrapMethod( + pkgInfo.name, + className || "default export" + ); + } return newInstance; }; diff --git a/library/sinks/Prisma.ts b/library/sinks/Prisma.ts index 325d30949..b0c4a5409 100644 --- a/library/sinks/Prisma.ts +++ b/library/sinks/Prisma.ts @@ -1,6 +1,5 @@ import type { Hooks } from "../agent/hooks/Hooks"; import { Wrapper } from "../agent/Wrapper"; -import { wrapExport } from "../agent/hooks/wrapExport"; import { wrapNewInstance } from "../agent/hooks/wrapNewInstance"; import { SQLDialect } from "../vulnerabilities/sql-injection/dialects/SQLDialect"; import { SQLDialectMySQL } from "../vulnerabilities/sql-injection/dialects/SQLDialectMySQL"; @@ -10,47 +9,71 @@ import { SQLDialectSQLite } from "../vulnerabilities/sql-injection/dialects/SQLD import type { InterceptorResult } from "../agent/hooks/InterceptorResult"; import { checkContextForSqlInjection } from "../vulnerabilities/sql-injection/checkContextForSqlInjection"; import { getContext } from "../agent/Context"; +import type { PrismaPromise } from "@prisma/client"; +import { onInspectionInterceptorResult } from "../agent/hooks/wrapExport"; +import { getInstance } from "../agent/AgentSingleton"; +import type { Agent } from "../agent/Agent"; +import { WrapPackageInfo } from "../agent/hooks/WrapPackageInfo"; + +type AllOperationsQueryExtension = { + model?: string; + operation: string; + args: any; + query: (args: any) => PrismaPromise; +}; export class Prisma implements Wrapper { - private rawSQLMethodsToWrap = ["$queryRawUnsafe", "$executeRawUnsafe"]; + private rawSQLMethodsToProtect = ["$queryRawUnsafe", "$executeRawUnsafe"]; + + // Check if the prisma client is a NoSQL client + private isNoSQLClient(clientInstance: any): boolean { + if ( + !clientInstance || + typeof clientInstance !== "object" || + !("_engineConfig" in clientInstance) || + !clientInstance._engineConfig || + typeof clientInstance._engineConfig !== "object" || + !("activeProvider" in clientInstance._engineConfig) || + typeof clientInstance._engineConfig.activeProvider !== "string" + ) { + return false; + } - private dialect: SQLDialect = new SQLDialectGeneric(); + return clientInstance._engineConfig.activeProvider === "mongodb"; + } // Try to detect the SQL dialect used by the Prisma client, so we can use the correct SQL dialect for the SQL injection detection. - private detectSQLDialect(clientInstance: any) { + private getClientSQLDialect(clientInstance: any): SQLDialect { // https://github.com/prisma/prisma/blob/559988a47e50b4d4655dc45b11ceb9b5c73ef053/packages/generator-helper/src/types.ts#L75 if ( !clientInstance || typeof clientInstance !== "object" || - !("_accelerateEngineConfig" in clientInstance) || - !clientInstance._accelerateEngineConfig || - typeof clientInstance._accelerateEngineConfig !== "object" || - !("activeProvider" in clientInstance._accelerateEngineConfig) || - typeof clientInstance._accelerateEngineConfig.activeProvider !== "string" + !("_engineConfig" in clientInstance) || + !clientInstance._engineConfig || + typeof clientInstance._engineConfig !== "object" || + !("activeProvider" in clientInstance._engineConfig) || + typeof clientInstance._engineConfig.activeProvider !== "string" ) { - return; + return new SQLDialectGeneric(); } - switch (clientInstance._accelerateEngineConfig.activeProvider) { + switch (clientInstance._engineConfig.activeProvider) { case "mysql": - this.dialect = new SQLDialectMySQL(); - break; + return new SQLDialectMySQL(); case "postgresql": case "postgres": - this.dialect = new SQLDialectPostgres(); - break; + return new SQLDialectPostgres(); case "sqlite": - this.dialect = new SQLDialectSQLite(); - break; + return new SQLDialectSQLite(); default: - // Already set to generic - break; + return new SQLDialectGeneric(); } } private inspectSQLQuery( args: unknown[], - operation: string + operation: string, + dialect: SQLDialect ): InterceptorResult { const context = getContext(); @@ -65,32 +88,89 @@ export class Prisma implements Wrapper { sql: sql, context: context, operation: `prisma.${operation}`, - dialect: this.dialect, + dialect: dialect, }); } return undefined; } + private onClientOperation({ + model, + operation, + args, + query, + isNoSQLClient, + sqlDialect, + agent, + pkgInfo, + }: AllOperationsQueryExtension & { + isNoSQLClient: boolean; + sqlDialect?: SQLDialect; + agent: Agent; + pkgInfo: WrapPackageInfo; + }) { + let inspectionResult: InterceptorResult | undefined; + const start = performance.now(); + + if (!isNoSQLClient && this.rawSQLMethodsToProtect.includes(operation)) { + inspectionResult = this.inspectSQLQuery( + args, + operation, + sqlDialect || new SQLDialectGeneric() + ); + } + + if (inspectionResult) { + onInspectionInterceptorResult( + getContext(), + agent, + inspectionResult, + pkgInfo, + start + ); + } + + return query(args); + } + wrap(hooks: Hooks) { hooks .addPackage("@prisma/client") .withVersion("^5.0.0") .onRequire((exports, pkgInfo) => { wrapNewInstance(exports, "PrismaClient", pkgInfo, (instance) => { - this.detectSQLDialect(instance); - - for (const method of this.rawSQLMethodsToWrap) { - if (typeof instance[method] === "function") { - wrapExport(instance, method, pkgInfo, { - inspectArgs: (args) => { - return this.inspectSQLQuery(args, method); - }, - }); - } + const isNoSQLClient = this.isNoSQLClient(instance); + + const agent = getInstance(); + if (!agent) { + return; } - // Todo support mongodb methods + // https://www.prisma.io/docs/orm/prisma-client/client-extensions/query#modify-all-operations-in-all-models-of-your-schema + return instance.$extends({ + query: { + $allOperations: ({ + model, + operation, + args, + query, + }: AllOperationsQueryExtension) => { + return this.onClientOperation({ + model, + operation, + args, + query, + isNoSQLClient, + sqlDialect: !isNoSQLClient + ? this.getClientSQLDialect(instance) + : undefined, + agent, + pkgInfo, + }); + }, + }, + }); }); }); } From 8d1793a36e4f093fb9b51482cc7e3191b09b326d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Mon, 25 Nov 2024 12:19:28 +0100 Subject: [PATCH 07/17] Protect raw Prisma MongoDB methods --- library/sinks/Prisma.test.ts | 74 +++++++++++++++++++++++++++++ library/sinks/Prisma.ts | 90 +++++++++++++++++++++++++++++++++--- 2 files changed, 158 insertions(+), 6 deletions(-) diff --git a/library/sinks/Prisma.test.ts b/library/sinks/Prisma.test.ts index 122eb7441..5166ed5d1 100644 --- a/library/sinks/Prisma.test.ts +++ b/library/sinks/Prisma.test.ts @@ -23,6 +23,23 @@ const context: Context = { route: "/posts/:id", }; +const noSQLContext: Context = { + remoteAddress: "::1", + method: "POST", + url: "http://localhost:4000", + query: {}, + headers: {}, + body: { + email: { + $ne: null, + }, + }, + cookies: {}, + routeParams: {}, + source: "express", + route: "/posts/:id", +}; + t.test("it works with sqlite", async (t) => { const agent = createTestAgent(); agent.start([new Prisma()]); @@ -249,6 +266,63 @@ t.test("it works with mongodb", async (t) => { }, }); + t.match(await client.user.findMany(), [ + { + email: "alice@example.com", + name: "Alice", + }, + ]); + + t.match( + await client.user.findRaw({ + filter: { + email: { $ne: null }, + }, + }), + [ + { + email: "alice@example.com", + name: "Alice", + }, + ] + ); + + await runWithContext(noSQLContext, async () => { + try { + await client.user.findRaw({ + filter: { + email: { $ne: null }, + }, + }); + t.fail("Execution should be blocked"); + } catch (error) { + t.ok(error instanceof Error); + if (error instanceof Error) { + t.same( + error.message, + "Zen has blocked a NoSQL injection: prisma.findRaw(...) originating from body.email" + ); + } + } + }); + + await runWithContext(noSQLContext, async () => { + try { + await client.user.aggregateRaw({ + pipeline: [{ $match: { email: { $ne: null } } }], + }); + t.fail("Execution should be blocked"); + } catch (error) { + t.ok(error instanceof Error); + if (error instanceof Error) { + t.same( + error.message, + "Zen has blocked a NoSQL injection: prisma.aggregateRaw(...) originating from body.email" + ); + } + } + }); + await client.user.deleteMany(); await client.$disconnect(); diff --git a/library/sinks/Prisma.ts b/library/sinks/Prisma.ts index b0c4a5409..9e3d9ea0e 100644 --- a/library/sinks/Prisma.ts +++ b/library/sinks/Prisma.ts @@ -8,23 +8,26 @@ import { SQLDialectPostgres } from "../vulnerabilities/sql-injection/dialects/SQ import { SQLDialectSQLite } from "../vulnerabilities/sql-injection/dialects/SQLDialectSQLite"; import type { InterceptorResult } from "../agent/hooks/InterceptorResult"; import { checkContextForSqlInjection } from "../vulnerabilities/sql-injection/checkContextForSqlInjection"; -import { getContext } from "../agent/Context"; -import type { PrismaPromise } from "@prisma/client"; +import { Context, getContext } from "../agent/Context"; import { onInspectionInterceptorResult } from "../agent/hooks/wrapExport"; import { getInstance } from "../agent/AgentSingleton"; import type { Agent } from "../agent/Agent"; import { WrapPackageInfo } from "../agent/hooks/WrapPackageInfo"; +import { detectNoSQLInjection } from "../vulnerabilities/nosql-injection/detectNoSQLInjection"; type AllOperationsQueryExtension = { model?: string; operation: string; args: any; - query: (args: any) => PrismaPromise; + query: (args: any) => Promise; }; -export class Prisma implements Wrapper { - private rawSQLMethodsToProtect = ["$queryRawUnsafe", "$executeRawUnsafe"]; +const NOSQL_OPERATIONS_WITH_FILTER = ["findRaw"] as const; +const NOSQL_OPERATIONS_WITH_PIPELINE = ["aggregateRaw"] as const; + +const SQL_OPERATIONS_TO_PROTECT = ["$queryRawUnsafe", "$executeRawUnsafe"]; +export class Prisma implements Wrapper { // Check if the prisma client is a NoSQL client private isNoSQLClient(clientInstance: any): boolean { if ( @@ -95,6 +98,75 @@ export class Prisma implements Wrapper { return undefined; } + private inspectNoSQLQuery( + args: unknown[], + operation: string, + model: string | undefined + ): InterceptorResult { + const context = getContext(); + if (!context) { + return undefined; + } + + if (!args || typeof args !== "object") { + return undefined; + } + + let filter; + + if ( + NOSQL_OPERATIONS_WITH_FILTER.includes(operation as any) && + "filter" in args + ) { + filter = args.filter; + } + + if ( + NOSQL_OPERATIONS_WITH_PIPELINE.includes(operation as any) && + "pipeline" in args + ) { + filter = args.pipeline; + } + + if (filter) { + return this.inspectNoSQLFilter( + model || "", + "", + context, + filter, + operation + ); + } + + return undefined; + } + + private inspectNoSQLFilter( + db: string, + collection: string, + request: Context, + filter: unknown, + operation: string + ): InterceptorResult { + const result = detectNoSQLInjection(request, filter); + + if (result.injection) { + return { + operation: `prisma.${operation}`, + kind: "nosql_injection", + source: result.source, + pathToPayload: result.pathToPayload, + metadata: { + db: db, + collection: collection, + operation: operation, + filter: JSON.stringify(filter), + }, + payload: result.payload, + }; + } + } + private onClientOperation({ model, operation, @@ -113,7 +185,7 @@ export class Prisma implements Wrapper { let inspectionResult: InterceptorResult | undefined; const start = performance.now(); - if (!isNoSQLClient && this.rawSQLMethodsToProtect.includes(operation)) { + if (!isNoSQLClient && SQL_OPERATIONS_TO_PROTECT.includes(operation)) { inspectionResult = this.inspectSQLQuery( args, operation, @@ -121,7 +193,12 @@ export class Prisma implements Wrapper { ); } + if (isNoSQLClient) { + inspectionResult = this.inspectNoSQLQuery(args, operation, model); + } + if (inspectionResult) { + // Run the logic to handle a detected attack onInspectionInterceptorResult( getContext(), agent, @@ -147,6 +224,7 @@ export class Prisma implements Wrapper { return; } + // Extend all operations of the Prisma client // https://www.prisma.io/docs/orm/prisma-client/client-extensions/query#modify-all-operations-in-all-models-of-your-schema return instance.$extends({ query: { 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 08/17] 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 From 4979c526a6d04963bd7c8ec2ae70fe209d745487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Mon, 25 Nov 2024 14:00:38 +0100 Subject: [PATCH 09/17] Fix e2e tests --- end2end/tests/hono-prisma.test.js | 2 ++ sample-apps/hono-prisma/app.js | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/end2end/tests/hono-prisma.test.js b/end2end/tests/hono-prisma.test.js index 94ff9a367..09352c630 100644 --- a/end2end/tests/hono-prisma.test.js +++ b/end2end/tests/hono-prisma.test.js @@ -10,6 +10,8 @@ const execAsync = promisify(execCb); const appDir = resolve(__dirname, "../../sample-apps/hono-prisma"); const pathToApp = join(appDir, "app.js"); +process.env.DATABASE_URL = "file:./dev.db"; + t.before(async (t) => { // Generate prismajs client const { stdout, stderr } = await execAsync( diff --git a/sample-apps/hono-prisma/app.js b/sample-apps/hono-prisma/app.js index 8a718ce0c..4e69b52d2 100644 --- a/sample-apps/hono-prisma/app.js +++ b/sample-apps/hono-prisma/app.js @@ -1,5 +1,7 @@ const Zen = require("@aikidosec/firewall"); +process.env.DATABASE_URL = "file:./dev.db"; + const { PrismaClient } = require("@prisma/client"); const { serve } = require("@hono/node-server"); const { Hono } = require("hono"); @@ -22,6 +24,8 @@ async function main() { const app = new Hono(); + Zen.addHonoMiddleware(app); + app.get("/", async (c) => { return c.text("Hello, world!"); }); From 8b4f1ca6cd6f2b50bd1588095a33efd251fbb701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Mon, 25 Nov 2024 15:06:28 +0100 Subject: [PATCH 10/17] Apply suggestions of reviewer --- .../hooks/onInspectionInterceptorResult.ts | 57 +++++++++++++++++ library/agent/hooks/wrapExport.ts | 62 ++----------------- library/sinks/Prisma.test.ts | 23 ++++++- library/sinks/Prisma.ts | 2 +- 4 files changed, 85 insertions(+), 59 deletions(-) create mode 100644 library/agent/hooks/onInspectionInterceptorResult.ts diff --git a/library/agent/hooks/onInspectionInterceptorResult.ts b/library/agent/hooks/onInspectionInterceptorResult.ts new file mode 100644 index 000000000..80123cd11 --- /dev/null +++ b/library/agent/hooks/onInspectionInterceptorResult.ts @@ -0,0 +1,57 @@ +import { resolve } from "path"; +import { cleanupStackTrace } from "../../helpers/cleanupStackTrace"; +import { escapeHTML } from "../../helpers/escapeHTML"; +import type { Agent } from "../Agent"; +import { attackKindHumanName } from "../Attack"; +import { getContext, updateContext } from "../Context"; +import type { InterceptorResult } from "./InterceptorResult"; +import type { WrapPackageInfo } from "./WrapPackageInfo"; + +// Used for cleaning up the stack trace +const libraryRoot = resolve(__dirname, "../.."); + +export function onInspectionInterceptorResult( + context: ReturnType, + agent: Agent, + result: InterceptorResult, + pkgInfo: WrapPackageInfo, + start: number +) { + const end = performance.now(); + agent.getInspectionStatistics().onInspectedCall({ + sink: pkgInfo.name, + attackDetected: !!result, + blocked: agent.shouldBlock(), + durationInMs: end - start, + withoutContext: !context, + }); + + const isAllowedIP = + context && + context.remoteAddress && + agent.getConfig().isAllowedIP(context.remoteAddress); + + if (result && context && !isAllowedIP) { + // Flag request as having an attack detected + updateContext(context, "attackDetected", true); + + agent.onDetectedAttack({ + module: pkgInfo.name, + operation: result.operation, + kind: result.kind, + source: result.source, + blocked: agent.shouldBlock(), + stack: cleanupStackTrace(new Error().stack!, libraryRoot), + path: result.pathToPayload, + metadata: result.metadata, + request: context, + payload: result.payload, + }); + + if (agent.shouldBlock()) { + throw new Error( + `Zen has blocked ${attackKindHumanName(result.kind)}: ${result.operation}(...) originating from ${result.source}${escapeHTML(result.pathToPayload)}` + ); + } + } +} diff --git a/library/agent/hooks/wrapExport.ts b/library/agent/hooks/wrapExport.ts index fd9fd8e1f..d687fa728 100644 --- a/library/agent/hooks/wrapExport.ts +++ b/library/agent/hooks/wrapExport.ts @@ -1,14 +1,11 @@ /* eslint-disable max-lines-per-function */ -import { resolve } from "path"; -import { cleanupStackTrace } from "../../helpers/cleanupStackTrace"; -import { escapeHTML } from "../../helpers/escapeHTML"; -import { Agent } from "../Agent"; +import type { Agent } from "../Agent"; import { getInstance } from "../AgentSingleton"; -import { attackKindHumanName } from "../Attack"; -import { bindContext, getContext, updateContext } from "../Context"; -import { InterceptorResult } from "./InterceptorResult"; -import { WrapPackageInfo } from "./WrapPackageInfo"; +import { bindContext, getContext } from "../Context"; +import type { InterceptorResult } from "./InterceptorResult"; +import type { WrapPackageInfo } from "./WrapPackageInfo"; import { wrapDefaultOrNamed } from "./wrapDefaultOrNamed"; +import { onInspectionInterceptorResult } from "./onInspectionInterceptorResult"; type InspectArgsInterceptor = ( args: unknown[], @@ -30,9 +27,6 @@ export type InterceptorObject = { modifyReturnValue?: ModifyReturnValueInterceptor; }; -// Used for cleaning up the stack trace -const libraryRoot = resolve(__dirname, "../.."); - /** * Wraps a function with the provided interceptors. * If the function is not part of an object, like default exports, pass undefined as methodName and the function as subject. @@ -156,49 +150,3 @@ function inspectArgs( } onInspectionInterceptorResult(context, agent, result, pkgInfo, start); } - -export function onInspectionInterceptorResult( - context: ReturnType, - agent: Agent, - result: InterceptorResult, - pkgInfo: WrapPackageInfo, - start: number -) { - const end = performance.now(); - agent.getInspectionStatistics().onInspectedCall({ - sink: pkgInfo.name, - attackDetected: !!result, - blocked: agent.shouldBlock(), - durationInMs: end - start, - withoutContext: !context, - }); - - const isAllowedIP = - context && - context.remoteAddress && - agent.getConfig().isAllowedIP(context.remoteAddress); - - if (result && context && !isAllowedIP) { - // Flag request as having an attack detected - updateContext(context, "attackDetected", true); - - agent.onDetectedAttack({ - module: pkgInfo.name, - operation: result.operation, - kind: result.kind, - source: result.source, - blocked: agent.shouldBlock(), - stack: cleanupStackTrace(new Error().stack!, libraryRoot), - path: result.pathToPayload, - metadata: result.metadata, - request: context, - payload: result.payload, - }); - - if (agent.shouldBlock()) { - throw new Error( - `Zen has blocked ${attackKindHumanName(result.kind)}: ${result.operation}(...) originating from ${result.source}${escapeHTML(result.pathToPayload)}` - ); - } - } -} diff --git a/library/sinks/Prisma.test.ts b/library/sinks/Prisma.test.ts index f47bcc153..2be423902 100644 --- a/library/sinks/Prisma.test.ts +++ b/library/sinks/Prisma.test.ts @@ -268,9 +268,28 @@ t.test("it works with mongodb", async (t) => { delete require.cache[key]; } + let operationCount = 0; + const { PrismaClient } = require("@prisma/client"); - const client = new PrismaClient(); + const client = new PrismaClient().$extends({ + query: { + $allOperations: ({ + model, + operation, + args, + query, + }: { + model?: string; + operation: string; + args: unknown; + query: (args: unknown) => Promise; + }) => { + operationCount++; + return query(args); + }, + }, + }); await client.user.create({ data: { @@ -336,6 +355,8 @@ t.test("it works with mongodb", async (t) => { } }); + t.same(operationCount, 3); + await client.user.deleteMany(); await client.$disconnect(); diff --git a/library/sinks/Prisma.ts b/library/sinks/Prisma.ts index 64b2cdcf6..ff53fcd82 100644 --- a/library/sinks/Prisma.ts +++ b/library/sinks/Prisma.ts @@ -9,7 +9,7 @@ import { SQLDialectSQLite } from "../vulnerabilities/sql-injection/dialects/SQLD import type { InterceptorResult } from "../agent/hooks/InterceptorResult"; import { checkContextForSqlInjection } from "../vulnerabilities/sql-injection/checkContextForSqlInjection"; import { Context, getContext } from "../agent/Context"; -import { onInspectionInterceptorResult } from "../agent/hooks/wrapExport"; +import { onInspectionInterceptorResult } from "../agent/hooks/onInspectionInterceptorResult"; import { getInstance } from "../agent/AgentSingleton"; import type { Agent } from "../agent/Agent"; import { WrapPackageInfo } from "../agent/hooks/WrapPackageInfo"; From 4fc83b2ddbe3e47881b33d268b16297dd2661996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Mon, 25 Nov 2024 16:48:58 +0100 Subject: [PATCH 11/17] Update t.fail usage in e2e tests Co-Authored-By: Hans Ott <3886384+hansott@users.noreply.github.com> --- end2end/tests/hono-prisma.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/end2end/tests/hono-prisma.test.js b/end2end/tests/hono-prisma.test.js index 09352c630..476714b95 100644 --- a/end2end/tests/hono-prisma.test.js +++ b/end2end/tests/hono-prisma.test.js @@ -36,7 +36,7 @@ t.test("it blocks in blocking mode", (t) => { }); server.on("error", (err) => { - t.fail(err.message); + t.fail(err); }); let stdout = ""; @@ -70,7 +70,7 @@ t.test("it blocks in blocking mode", (t) => { t.match(stderr, /Zen has blocked an SQL injection/); }) .catch((error) => { - t.fail(error.message); + t.fail(error); }) .finally(() => { server.kill(); @@ -87,7 +87,7 @@ t.test("it does not block in non-blocking mode", (t) => { }); server.on("error", (err) => { - t.fail(err.message); + t.fail(err); }); let stdout = ""; @@ -121,7 +121,7 @@ t.test("it does not block in non-blocking mode", (t) => { t.notMatch(stderr, /Zen has blocked an SQL injection/); }) .catch((error) => { - t.fail(error.message); + t.fail(error); }) .finally(() => { server.kill(); From 33f069c3879ed87bf2e8f5ffb4ca36df0b7fa8e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Fri, 29 Nov 2024 16:02:37 +0100 Subject: [PATCH 12/17] Support prisma v6 --- library/package-lock.json | 68 +++++++++++++++++++-------------------- library/package.json | 12 +++---- library/sinks/Prisma.ts | 2 +- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/library/package-lock.json b/library/package-lock.json index 58bb8eebf..6a677eb30 100644 --- a/library/package-lock.json +++ b/library/package-lock.json @@ -18,7 +18,7 @@ "@hono/node-server": "^1.12.2", "@koa/bodyparser": "^5.1.1", "@koa/router": "^13.0.0", - "@prisma/client": "^5.22.0", + "@prisma/client": "^6.0.0", "@sinonjs/fake-timers": "^11.2.2", "@types/aws-lambda": "^8.10.131", "@types/cookie-parser": "^1.4.6", @@ -71,7 +71,7 @@ "pg": "^8.11.3", "postgres": "^3.4.4", "prettier": "^3.2.4", - "prisma": "^5.22.0", + "prisma": "^6.0.0", "shell-quote": "^1.8.1", "shelljs": "^0.8.5", "sqlite3": "^5.1.7", @@ -2921,14 +2921,14 @@ } }, "node_modules/@prisma/client": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", - "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.0.0.tgz", + "integrity": "sha512-tOBhG35ozqZ/5Y6B0TNOa6cwULUW8ijXqBXcgb12bfozqf6eGMyGs+jphywCsj6uojv5lAZZnxVSoLMVebIP+g==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "engines": { - "node": ">=16.13" + "node": ">=18.18" }, "peerDependencies": { "prisma": "*" @@ -2940,53 +2940,53 @@ } }, "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==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.0.0.tgz", + "integrity": "sha512-eUjoNThlDXdyJ1iQ2d7U6aTVwm59EwvODb5zFVNJEokNoSiQmiYWNzZIwZyDmZ+j51j42/0iTaHIJ4/aZPKFRg==", "dev": 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==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.0.0.tgz", + "integrity": "sha512-ZZCVP3q22ifN6Ex6C8RIcTDBlRtMJS2H1ljV0knCiWNGArvvkEbE88W3uDdq/l4+UvyvHpGzdf9ZsCWSQR7ZQQ==", "dev": 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" + "@prisma/debug": "6.0.0", + "@prisma/engines-version": "5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e", + "@prisma/fetch-engine": "6.0.0", + "@prisma/get-platform": "6.0.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==", + "version": "5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e.tgz", + "integrity": "sha512-JmIds0Q2/vsOmnuTJYxY4LE+sajqjYKhLtdOT6y4imojqv5d/aeVEfbBGC74t8Be1uSp0OP8lxIj2OqoKbLsfQ==", "dev": 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==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.0.0.tgz", + "integrity": "sha512-j2m+iO5RDPRI7SUc7sHo8wX7SA4iTkJ+18Sxch8KinQM46YiCQD1iXKN6qU79C1Fliw5Bw/qDyTHaTsa3JMerA==", "dev": 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" + "@prisma/debug": "6.0.0", + "@prisma/engines-version": "5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e", + "@prisma/get-platform": "6.0.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==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.0.0.tgz", + "integrity": "sha512-PS6nYyIm9g8C03E4y7LknOfdCw/t2KyEJxntMPQHQZCOUgOpF82Ma60mdlOD08w90I3fjLiZZ0+MadenR3naDQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0" + "@prisma/debug": "6.0.0" } }, "node_modules/@protobufjs/aspromise": { @@ -13597,20 +13597,20 @@ } }, "node_modules/prisma": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", - "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.0.0.tgz", + "integrity": "sha512-RX7KtbW7IoEByf7MR32JK1PkVYLVYFqeODTtiIX3cqekq1aKdsF3Eud4zp2sUShMLjvdb5Jow0LbUjRq5LVxPw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/engines": "5.22.0" + "@prisma/engines": "6.0.0" }, "bin": { "prisma": "build/index.js" }, "engines": { - "node": ">=16.13" + "node": ">=18.18" }, "optionalDependencies": { "fsevents": "2.3.3" diff --git a/library/package.json b/library/package.json index bf5cd1519..7eeef1cb9 100644 --- a/library/package.json +++ b/library/package.json @@ -42,7 +42,7 @@ "@hono/node-server": "^1.12.2", "@koa/bodyparser": "^5.1.1", "@koa/router": "^13.0.0", - "@prisma/client": "^5.22.0", + "@prisma/client": "^6.0.0", "@sinonjs/fake-timers": "^11.2.2", "@types/aws-lambda": "^8.10.131", "@types/cookie-parser": "^1.4.6", @@ -72,9 +72,9 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^5.1.3", "express": "^5.0.0", + "express-async-handler": "^1.2.0", "express-v4": "npm:express@^4.0.0", "express-v5": "npm:express@^5.0.0", - "express-async-handler": "^1.2.0", "fast-xml-parser": "^4.4.0", "fastify": "^5.0.0", "follow-redirects": "^1.15.6", @@ -95,7 +95,7 @@ "pg": "^8.11.3", "postgres": "^3.4.4", "prettier": "^3.2.4", - "prisma": "^5.22.0", + "prisma": "^6.0.0", "shell-quote": "^1.8.1", "shelljs": "^0.8.5", "sqlite3": "^5.1.7", @@ -103,12 +103,12 @@ "tap": "^18.6.1", "type-fest": "^4.24.0", "typescript": "^5.3.3", - "xml-js": "^1.6.11", - "xml2js": "^0.6.2", "undici-v4": "npm:undici@^4.0.0", "undici-v5": "npm:undici@^5.0.0", "undici-v6": "npm:undici@^6.0.0", - "undici-v7": "npm:undici@^7.0.0" + "undici-v7": "npm:undici@^7.0.0", + "xml-js": "^1.6.11", + "xml2js": "^0.6.2" }, "scripts": { "test": "node ../scripts/run-tap.js", diff --git a/library/sinks/Prisma.ts b/library/sinks/Prisma.ts index ff53fcd82..291057776 100644 --- a/library/sinks/Prisma.ts +++ b/library/sinks/Prisma.ts @@ -206,7 +206,7 @@ export class Prisma implements Wrapper { wrap(hooks: Hooks) { hooks .addPackage("@prisma/client") - .withVersion("^5.0.0") + .withVersion("^5.0.0 || ^6.0.0") .onRequire((exports, pkgInfo) => { wrapNewInstance(exports, "PrismaClient", pkgInfo, (instance) => { const isNoSQLClient = this.isNoSQLClient(instance); From 31be3d00e9327a966b5a4ee739a281fa3c88b60d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Fri, 29 Nov 2024 16:11:35 +0100 Subject: [PATCH 13/17] Skip tests on Node v16 --- library/sinks/Prisma.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/sinks/Prisma.test.ts b/library/sinks/Prisma.test.ts index 2be423902..fdf45620a 100644 --- a/library/sinks/Prisma.test.ts +++ b/library/sinks/Prisma.test.ts @@ -5,6 +5,7 @@ import { createTestAgent } from "../helpers/createTestAgent"; import { promisify } from "util"; import { exec as execCb } from "child_process"; import * as path from "path"; +import { getMajorNodeVersion } from "../helpers/getNodeVersion"; const execAsync = promisify(execCb); @@ -40,7 +41,12 @@ const noSQLContext: Context = { route: "/posts/:id", }; -t.test("it works with sqlite", async (t) => { +const testOpts = { + skip: + getMajorNodeVersion() < 18 ? "Prisma does not support Node.js < 18" : false, +}; + +t.test("it works with sqlite", testOpts, async (t) => { const agent = createTestAgent(); agent.start([new Prisma()]); @@ -145,7 +151,7 @@ t.test("it works with sqlite", async (t) => { await client.$disconnect(); }); -t.test("it works with postgres", async (t) => { +t.test("it works with postgres", testOpts, async (t) => { const agent = createTestAgent(); agent.start([new Prisma()]); @@ -244,7 +250,7 @@ t.test("it works with postgres", async (t) => { await client.$disconnect(); }); -t.test("it works with mongodb", async (t) => { +t.test("it works with mongodb", testOpts, async (t) => { const agent = createTestAgent(); agent.start([new Prisma()]); From 09b4c5b68107d3783276e2d8e35ecfd55a856d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Fri, 13 Dec 2024 14:47:21 +0100 Subject: [PATCH 14/17] Fix Prisma tests using Node v16 --- library/package-lock.json | 183 +++++++++++----------- library/package.json | 4 +- library/sinks/Prisma.test.ts | 6 +- sample-apps/hono-prisma/package-lock.json | 86 +++++----- sample-apps/hono-prisma/package.json | 4 +- 5 files changed, 138 insertions(+), 145 deletions(-) diff --git a/library/package-lock.json b/library/package-lock.json index 844430b09..593152104 100644 --- a/library/package-lock.json +++ b/library/package-lock.json @@ -18,7 +18,7 @@ "@hono/node-server": "^1.12.2", "@koa/bodyparser": "^5.1.1", "@koa/router": "^13.0.0", - "@prisma/client": "^6.0.0", + "@prisma/client": "^5.22.0", "@sinonjs/fake-timers": "^11.2.2", "@types/aws-lambda": "^8.10.131", "@types/cookie-parser": "^1.4.6", @@ -71,7 +71,7 @@ "pg": "^8.11.3", "postgres": "^3.4.4", "prettier": "^3.2.4", - "prisma": "^6.0.0", + "prisma": "^5.22.0", "shell-quote": "^1.8.1", "shelljs": "^0.8.5", "sqlite3": "^5.1.7", @@ -1168,20 +1168,19 @@ "optional": true }, "node_modules/@google-cloud/functions-framework": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-3.4.2.tgz", - "integrity": "sha512-yJcxfVgjLoKFO3p6Wy6Fc+Gi6l3PFSwJg4m0mjebx/UHdLeXLYYxgKMP8RCODaApXEWXbSITIjXO0m5kSv2Ilw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-3.4.3.tgz", + "integrity": "sha512-NR9r3cAcuE35bW+23jRrYXWY7QFwK4UlLtmf6y+JgKzplt7s50gPrROuD2yqB9JKXynXfdRAC0EwwHZ+4Q0jsQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@types/express": "4.17.21", + "@types/express": "^5.0.0", "body-parser": "^1.18.3", - "cloudevents": "^8.0.0", - "express": "^4.16.4", - "minimist": "^1.2.7", + "cloudevents": "^8.0.2", + "express": "^4.21.2", + "minimist": "^1.2.8", "on-finished": "^2.3.0", "read-pkg-up": "^7.0.1", - "semver": "^7.3.5" + "semver": "^7.6.3" }, "bin": { "functions-framework": "build/src/main.js", @@ -1191,12 +1190,35 @@ "node": ">=10.0.0" } }, + "node_modules/@google-cloud/functions-framework/node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@google-cloud/functions-framework/node_modules/@types/express-serve-static-core": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", + "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@google-cloud/functions-framework/node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, - "license": "MIT", "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -1209,15 +1231,13 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@google-cloud/functions-framework/node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -1230,7 +1250,6 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1239,15 +1258,13 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@google-cloud/functions-framework/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -1256,15 +1273,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@google-cloud/functions-framework/node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, - "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -1285,7 +1300,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -1300,6 +1315,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@google-cloud/functions-framework/node_modules/finalhandler": { @@ -1307,7 +1326,6 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, - "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -1326,7 +1344,6 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1336,7 +1353,6 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -1346,7 +1362,6 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, - "license": "MIT", "bin": { "mime": "cli.js" }, @@ -1359,7 +1374,6 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1369,7 +1383,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -1382,24 +1395,21 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/@google-cloud/functions-framework/node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", - "dev": true, - "license": "MIT" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true }, "node_modules/@google-cloud/functions-framework/node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, - "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -1424,7 +1434,6 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1434,7 +1443,6 @@ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, - "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", @@ -2921,14 +2929,13 @@ } }, "node_modules/@prisma/client": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.0.0.tgz", - "integrity": "sha512-tOBhG35ozqZ/5Y6B0TNOa6cwULUW8ijXqBXcgb12bfozqf6eGMyGs+jphywCsj6uojv5lAZZnxVSoLMVebIP+g==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", "dev": true, "hasInstallScript": true, - "license": "Apache-2.0", "engines": { - "node": ">=18.18" + "node": ">=16.13" }, "peerDependencies": { "prisma": "*" @@ -2940,53 +2947,48 @@ } }, "node_modules/@prisma/debug": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.0.0.tgz", - "integrity": "sha512-eUjoNThlDXdyJ1iQ2d7U6aTVwm59EwvODb5zFVNJEokNoSiQmiYWNzZIwZyDmZ+j51j42/0iTaHIJ4/aZPKFRg==", - "dev": true, - "license": "Apache-2.0" + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "dev": true }, "node_modules/@prisma/engines": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.0.0.tgz", - "integrity": "sha512-ZZCVP3q22ifN6Ex6C8RIcTDBlRtMJS2H1ljV0knCiWNGArvvkEbE88W3uDdq/l4+UvyvHpGzdf9ZsCWSQR7ZQQ==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", "dev": true, "hasInstallScript": true, - "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.0.0", - "@prisma/engines-version": "5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e", - "@prisma/fetch-engine": "6.0.0", - "@prisma/get-platform": "6.0.0" + "@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.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e.tgz", - "integrity": "sha512-JmIds0Q2/vsOmnuTJYxY4LE+sajqjYKhLtdOT6y4imojqv5d/aeVEfbBGC74t8Be1uSp0OP8lxIj2OqoKbLsfQ==", - "dev": true, - "license": "Apache-2.0" + "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==", + "dev": true }, "node_modules/@prisma/fetch-engine": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.0.0.tgz", - "integrity": "sha512-j2m+iO5RDPRI7SUc7sHo8wX7SA4iTkJ+18Sxch8KinQM46YiCQD1iXKN6qU79C1Fliw5Bw/qDyTHaTsa3JMerA==", + "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==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.0.0", - "@prisma/engines-version": "5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e", - "@prisma/get-platform": "6.0.0" + "@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": "6.0.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.0.0.tgz", - "integrity": "sha512-PS6nYyIm9g8C03E4y7LknOfdCw/t2KyEJxntMPQHQZCOUgOpF82Ma60mdlOD08w90I3fjLiZZ0+MadenR3naDQ==", + "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==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.0.0" + "@prisma/debug": "5.22.0" } }, "node_modules/@protobufjs/aspromise": { @@ -8025,9 +8027,9 @@ }, "node_modules/express-v4": { "name": "express", - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, "dependencies": { "accepts": "~1.3.8", @@ -8049,7 +8051,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -8064,6 +8066,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-v4/node_modules/accepts": { @@ -8206,9 +8212,9 @@ } }, "node_modules/express-v4/node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "dev": true }, "node_modules/express-v4/node_modules/send": { @@ -13597,20 +13603,19 @@ } }, "node_modules/prisma": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.0.0.tgz", - "integrity": "sha512-RX7KtbW7IoEByf7MR32JK1PkVYLVYFqeODTtiIX3cqekq1aKdsF3Eud4zp2sUShMLjvdb5Jow0LbUjRq5LVxPw==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", "dev": true, "hasInstallScript": true, - "license": "Apache-2.0", "dependencies": { - "@prisma/engines": "6.0.0" + "@prisma/engines": "5.22.0" }, "bin": { "prisma": "build/index.js" }, "engines": { - "node": ">=18.18" + "node": ">=16.13" }, "optionalDependencies": { "fsevents": "2.3.3" diff --git a/library/package.json b/library/package.json index 0c0de85ca..810670a38 100644 --- a/library/package.json +++ b/library/package.json @@ -51,7 +51,7 @@ "@hono/node-server": "^1.12.2", "@koa/bodyparser": "^5.1.1", "@koa/router": "^13.0.0", - "@prisma/client": "^6.0.0", + "@prisma/client": "^5.22.0", "@sinonjs/fake-timers": "^11.2.2", "@types/aws-lambda": "^8.10.131", "@types/cookie-parser": "^1.4.6", @@ -104,7 +104,7 @@ "pg": "^8.11.3", "postgres": "^3.4.4", "prettier": "^3.2.4", - "prisma": "^6.0.0", + "prisma": "^5.22.0", "shell-quote": "^1.8.1", "shelljs": "^0.8.5", "sqlite3": "^5.1.7", diff --git a/library/sinks/Prisma.test.ts b/library/sinks/Prisma.test.ts index fdf45620a..f9648b20d 100644 --- a/library/sinks/Prisma.test.ts +++ b/library/sinks/Prisma.test.ts @@ -5,7 +5,6 @@ import { createTestAgent } from "../helpers/createTestAgent"; import { promisify } from "util"; import { exec as execCb } from "child_process"; import * as path from "path"; -import { getMajorNodeVersion } from "../helpers/getNodeVersion"; const execAsync = promisify(execCb); @@ -41,10 +40,7 @@ const noSQLContext: Context = { route: "/posts/:id", }; -const testOpts = { - skip: - getMajorNodeVersion() < 18 ? "Prisma does not support Node.js < 18" : false, -}; +const testOpts = {}; t.test("it works with sqlite", testOpts, async (t) => { const agent = createTestAgent(); diff --git a/sample-apps/hono-prisma/package-lock.json b/sample-apps/hono-prisma/package-lock.json index 04e623fab..640b63cc3 100644 --- a/sample-apps/hono-prisma/package-lock.json +++ b/sample-apps/hono-prisma/package-lock.json @@ -8,11 +8,11 @@ "dependencies": { "@aikidosec/firewall": "file:../../build", "@hono/node-server": "^1.11.2", - "@prisma/client": "^5.22.0", + "@prisma/client": "^6.0.1", "hono": "^4.6.8" }, "devDependencies": { - "prisma": "^5.22.0" + "prisma": "^6.0.1" } }, "../../build": { @@ -40,13 +40,12 @@ } }, "node_modules/@prisma/client": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", - "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.0.1.tgz", + "integrity": "sha512-60w7kL6bUxz7M6Gs/V+OWMhwy94FshpngVmOY05TmGD0Lhk+Ac0ZgtjlL6Wll9TD4G03t4Sq1wZekNVy+Xdlbg==", "hasInstallScript": true, - "license": "Apache-2.0", "engines": { - "node": ">=16.13" + "node": ">=18.18" }, "peerDependencies": { "prisma": "*" @@ -58,53 +57,48 @@ } }, "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" + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.0.1.tgz", + "integrity": "sha512-jQylgSOf7ibTVxqBacnAlVGvek6fQxJIYCQOeX2KexsfypNzXjJQSS2o5s+Mjj2Np93iSOQUaw6TvPj8syhG4w==", + "devOptional": true }, "node_modules/@prisma/engines": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", - "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.0.1.tgz", + "integrity": "sha512-4hxzI+YQIR2uuDyVsDooFZGu5AtixbvM2psp+iayDZ4hRrAHo/YwgA17N23UWq7G6gRu18NvuNMb48qjP3DPQw==", "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" + "@prisma/debug": "6.0.1", + "@prisma/engines-version": "5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e", + "@prisma/fetch-engine": "6.0.1", + "@prisma/get-platform": "6.0.1" } }, "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" + "version": "5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e.tgz", + "integrity": "sha512-JmIds0Q2/vsOmnuTJYxY4LE+sajqjYKhLtdOT6y4imojqv5d/aeVEfbBGC74t8Be1uSp0OP8lxIj2OqoKbLsfQ==", + "devOptional": true }, "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==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.0.1.tgz", + "integrity": "sha512-T36bWFVGeGYYSyYOj9d+O9G3sBC+pAyMC+jc45iSL63/Haq1GrYjQPgPMxrEj9m739taXrupoysRedQ+VyvM/Q==", "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" + "@prisma/debug": "6.0.1", + "@prisma/engines-version": "5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e", + "@prisma/get-platform": "6.0.1" } }, "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==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.0.1.tgz", + "integrity": "sha512-zspC9vlxAqx4E6epMPMLLBMED2VD8axDe8sPnquZ8GOsn6tiacWK0oxrGK4UAHYzYUVuMVUApJbdXB2dFpLhvg==", "devOptional": true, - "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0" + "@prisma/debug": "6.0.1" } }, "node_modules/fsevents": { @@ -123,29 +117,27 @@ } }, "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", + "version": "4.6.13", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.6.13.tgz", + "integrity": "sha512-haV0gaMdSjy9URCRN9hxBPlqHa7fMm/T72kAImIxvw4eQLbNz1rgjN4hHElLJSieDiNuiIAXC//cC6YGz2KCbg==", "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==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.0.1.tgz", + "integrity": "sha512-CaMNFHkf+DDq8zq3X/JJsQ4Koy7dyWwwtOKibkT/Am9j/tDxcfbg7+lB1Dzhx18G/+RQCMgjPYB61bhRqteNBQ==", "devOptional": true, "hasInstallScript": true, - "license": "Apache-2.0", "dependencies": { - "@prisma/engines": "5.22.0" + "@prisma/engines": "6.0.1" }, "bin": { "prisma": "build/index.js" }, "engines": { - "node": ">=16.13" + "node": ">=18.18" }, "optionalDependencies": { "fsevents": "2.3.3" diff --git a/sample-apps/hono-prisma/package.json b/sample-apps/hono-prisma/package.json index 97c2f13df..d9307d4fe 100644 --- a/sample-apps/hono-prisma/package.json +++ b/sample-apps/hono-prisma/package.json @@ -3,10 +3,10 @@ "dependencies": { "@aikidosec/firewall": "file:../../build", "@hono/node-server": "^1.11.2", - "@prisma/client": "^5.22.0", + "@prisma/client": "^6.0.1", "hono": "^4.6.8" }, "devDependencies": { - "prisma": "^5.22.0" + "prisma": "^6.0.1" } } From 2f35bee1274bd855b91f6d9e2afa8e8b271afb1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Fri, 13 Dec 2024 14:55:40 +0100 Subject: [PATCH 15/17] Add install-lib-only to Makefile --- .github/workflows/lint-code.yml | 2 +- .github/workflows/unit-test.yml | 2 +- Makefile | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint-code.yml b/.github/workflows/lint-code.yml index 68850ce1d..d4d254903 100644 --- a/.github/workflows/lint-code.yml +++ b/.github/workflows/lint-code.yml @@ -12,6 +12,6 @@ jobs: uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - - run: make install + - run: make install-lib-only - run: make build - run: make lint diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 3ee27d140..026058e56 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -74,7 +74,7 @@ jobs: - name: Add local.aikido.io to /etc/hosts run: | sudo echo "127.0.0.1 local.aikido.io" | sudo tee -a /etc/hosts - - run: make install + - run: make install-lib-only - run: make build - run: make test-ci - name: "Upload coverage" diff --git a/Makefile b/Makefile index 87116b624..a2e8cdfee 100644 --- a/Makefile +++ b/Makefile @@ -88,14 +88,17 @@ fastify-clickhouse: hono-prisma: cd sample-apps/hono-prisma && AIKIDO_DEBUG=true AIKIDO_BLOCK=true node app.js -.PHONY: install -install: +.PHONY: install-lib-only +install-lib-only: mkdir -p build node scripts/copyPackageJSON.js touch build/index.js cd build && npm link npm install cd library && npm install + +.PHONY: install +install: install-lib-only cd end2end && npm install node scripts/install.js From 0ac3238a0cca9ce42fb4ad27ec4fcbd8250454e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Fri, 13 Dec 2024 15:07:05 +0100 Subject: [PATCH 16/17] Fix unit tests --- library/helpers/getPackageVersion.test.ts | 2 +- library/sinks/Prisma.test.ts | 18 +++--------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/library/helpers/getPackageVersion.test.ts b/library/helpers/getPackageVersion.test.ts index 1b3282202..1a7556f72 100644 --- a/library/helpers/getPackageVersion.test.ts +++ b/library/helpers/getPackageVersion.test.ts @@ -4,5 +4,5 @@ import { getPackageVersion } from "./getPackageVersion"; t.test("it resolves the version of a package", async (t) => { t.same(getPackageVersion("express"), "5.0.1"); t.same(getPackageVersion("non-existing-package"), null); - t.same(getPackageVersion("@google-cloud/functions-framework"), "3.4.2"); + t.same(getPackageVersion("@google-cloud/functions-framework"), "3.4.3"); }); diff --git a/library/sinks/Prisma.test.ts b/library/sinks/Prisma.test.ts index f9648b20d..30a4949ad 100644 --- a/library/sinks/Prisma.test.ts +++ b/library/sinks/Prisma.test.ts @@ -49,17 +49,13 @@ t.test("it works with sqlite", testOpts, async (t) => { process.env.DATABASE_URL = "file:./dev.db"; // Generate prismajs client - const { stdout, stderr } = await execAsync( + await execAsync( "npx prisma migrate reset --force", // Generate prisma client, reset db and apply migrations { cwd: path.join(__dirname, "fixtures/prisma/sqlite"), } ); - if (stderr) { - t.fail(stderr); - } - const { PrismaClient } = require("@prisma/client"); const client = new PrismaClient(); @@ -154,17 +150,13 @@ t.test("it works with postgres", testOpts, async (t) => { process.env.DATABASE_URL = "postgres://root:password@127.0.0.1:27016/main_db"; // Generate prismajs client - const { stdout, stderr } = await execAsync( + await execAsync( "npx prisma migrate reset --force", // Generate prisma client, reset db and apply migrations { cwd: path.join(__dirname, "fixtures/prisma/postgres"), } ); - if (stderr) { - t.fail(stderr); - } - // Clear require cache for (const key in require.cache) { delete require.cache[key]; @@ -254,17 +246,13 @@ t.test("it works with mongodb", testOpts, async (t) => { "mongodb://root:password@127.0.0.1:27020/prisma?authSource=admin&directConnection=true"; // Generate prismajs client - const { stdout, stderr } = await execAsync( + await execAsync( "npx prisma generate", // Generate prisma client, reset db and apply migrations { cwd: path.join(__dirname, "fixtures/prisma/mongodb"), } ); - if (stderr) { - t.fail(stderr); - } - // Clear require cache for (const key in require.cache) { delete require.cache[key]; From a8f910bc37d3bb45b6cf08d0883ca759978e43aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Wed, 18 Dec 2024 15:19:42 +0100 Subject: [PATCH 17/17] Fix merge --- library/agent/hooks/onInspectionInterceptorResult.ts | 4 ++-- library/sinks/Prisma.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/agent/hooks/onInspectionInterceptorResult.ts b/library/agent/hooks/onInspectionInterceptorResult.ts index 80123cd11..1e64fb2fc 100644 --- a/library/agent/hooks/onInspectionInterceptorResult.ts +++ b/library/agent/hooks/onInspectionInterceptorResult.ts @@ -42,7 +42,7 @@ export function onInspectionInterceptorResult( source: result.source, blocked: agent.shouldBlock(), stack: cleanupStackTrace(new Error().stack!, libraryRoot), - path: result.pathToPayload, + paths: result.pathsToPayload, metadata: result.metadata, request: context, payload: result.payload, @@ -50,7 +50,7 @@ export function onInspectionInterceptorResult( if (agent.shouldBlock()) { throw new Error( - `Zen has blocked ${attackKindHumanName(result.kind)}: ${result.operation}(...) originating from ${result.source}${escapeHTML(result.pathToPayload)}` + `Zen has blocked ${attackKindHumanName(result.kind)}: ${result.operation}(...) originating from ${result.source}${escapeHTML((result.pathsToPayload || []).join())}` ); } } diff --git a/library/sinks/Prisma.ts b/library/sinks/Prisma.ts index 291057776..94a773afb 100644 --- a/library/sinks/Prisma.ts +++ b/library/sinks/Prisma.ts @@ -148,7 +148,7 @@ export class Prisma implements Wrapper { operation: `prisma.${operation}`, kind: "nosql_injection", source: result.source, - pathToPayload: result.pathToPayload, + pathsToPayload: result.pathsToPayload, metadata: { collection: collection, operation: operation,