diff --git a/library/sinks/Prisma.sqlite.test.ts b/library/sinks/Prisma.sqlite.test.ts deleted file mode 100644 index 55bb50f9..00000000 --- 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 00000000..0e898870 --- /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 00000000..93777787 --- /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 00000000..fbffa92c --- /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 00000000..23051681 --- /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