Skip to content

Commit

Permalink
Add initial prisma support
Browse files Browse the repository at this point in the history
  • Loading branch information
timokoessler committed Nov 22, 2024
1 parent e5c5a08 commit 08638c1
Show file tree
Hide file tree
Showing 10 changed files with 323 additions and 0 deletions.
2 changes: 2 additions & 0 deletions library/agent/protect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -136,6 +137,7 @@ function getWrappers() {
new Fastify(),
new Koa(),
new ClickHouse(),
new Prisma(),
];
}

Expand Down
91 changes: 91 additions & 0 deletions library/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
75 changes: 75 additions & 0 deletions library/sinks/Prisma.test.ts
Original file line number Diff line number Diff line change
@@ -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: "[email protected]",
},
});

t.same(await client.$queryRawUnsafe("SELECT * FROM USER"), [
{
id: user.id,
name: "Alice",
email: "[email protected]",
},
]);

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"
);
}
}
});
});
94 changes: 94 additions & 0 deletions library/sinks/Prisma.ts
Original file line number Diff line number Diff line change
@@ -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

Check failure on line 90 in library/sinks/Prisma.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Unexpected 'todo' comment: 'Todo support mongodb methods'
});
});
}
}
3 changes: 3 additions & 0 deletions library/sinks/fixtures/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
*.db
*.db-journal
Original file line number Diff line number Diff line change
@@ -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");
3 changes: 3 additions & 0 deletions library/sinks/fixtures/prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -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"
27 changes: 27 additions & 0 deletions library/sinks/fixtures/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SQLDialect } from "./SQLDialect";

export class SQLDialectGeneric implements SQLDialect {
getWASMDialectInt(): number {
return 0;
}
}

0 comments on commit 08638c1

Please sign in to comment.