Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions library/agent/hooks/wrapNewInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function wrapNewInstance(
subject: unknown,
className: string | undefined,
pkgInfo: PartialWrapPackageInfo,
interceptor: (exports: any) => unknown
interceptor: (instance: any, constructorArgs: unknown[]) => unknown
) {
const agent = getInstance();
if (!agent) {
Expand All @@ -29,7 +29,7 @@ export function wrapNewInstance(
const newInstance = new original(...args);

try {
const returnVal = interceptor(newInstance);
const returnVal = interceptor(newInstance, args);
if (returnVal) {
return returnVal;
}
Expand Down
91 changes: 91 additions & 0 deletions library/sinks/Prisma.eventLogging.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as t from "tap";
import { wrap } from "../helpers/wrap";
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 { join } from "path";
import { LockFile } from "../helpers/LockFile";

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 does not instrument when event-based logging is enabled",
async (t) => {
const lock = new LockFile("prisma-shared");

await lock.withLock(async () => {
const agent = createTestAgent();
agent.start([new Prisma()]);

process.env.DATABASE_URL =
"postgres://root:password@127.0.0.1:27016/main_db";

await execAsync("npx prisma migrate reset --force", {
cwd: join(__dirname, "fixtures/prisma/postgres"),
});

const { PrismaClient } = require("@prisma/client");

const logs: string[] = [];
wrap(console, "warn", function warn() {
return function warn(message: string) {
logs.push(message);
};
});

// Create client with event-based logging
const client = new PrismaClient({
log: [{ emit: "event", level: "query" }],
});

// Should have logged a warning
t.ok(
logs.some((w) => w.includes("AIKIDO: Prisma instrumentation disabled")),
"Should log warning about disabled instrumentation"
);

// $on() should work (wouldn't work if we extended the client)
let queryEventFired = false;
client.$on("query", () => {
queryEventFired = true;
});

await client.appUser.findMany();

t.ok(
queryEventFired,
"$on() should work when instrumentation is disabled"
);

// SQL injection should NOT be blocked (because instrumentation is disabled)
await runWithContext(context, async () => {
const result = await client.$queryRawUnsafe(
'SELECT * FROM "AppUser" -- should be blocked'
);
t.ok(
Array.isArray(result),
"Query should not be blocked when instrumentation is disabled"
);
});

await client.$disconnect();
});
}
);
42 changes: 37 additions & 5 deletions library/sinks/Prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { PartialWrapPackageInfo } from "../agent/hooks/WrapPackageInfo";
import { detectNoSQLInjection } from "../vulnerabilities/nosql-injection/detectNoSQLInjection";
import type { LocalVariableAccessConfig } from "../agent/hooks/instrumentation/types";
import { inspectArgs } from "../agent/hooks/wrapExport";
import { isPlainObject } from "../helpers/isPlainObject";

type AllOperationsQueryExtension = {
model?: string;
Expand Down Expand Up @@ -200,10 +201,41 @@ export class Prisma implements Wrapper {
return query(args);
}

// Check if the Prisma client uses event-based logging (emit: 'event')
// which requires $on() to work. Since $extends() breaks $on(), we can't
// instrument clients that use event-based logging.
// See: https://github.com/prisma/prisma/issues/24070
private usesEventBasedLogging(constructorArgs: unknown[]): boolean {
if (constructorArgs.length === 0) {
return false;
}

const options = constructorArgs[0];
if (!isPlainObject(options) || !Array.isArray(options.log)) {
return false;
}

return options.log.some(
(entry) => isPlainObject(entry) && entry.emit === "event"
);
}

private instrumentPrismaClient(
instance: any,
pkgInfo: PartialWrapPackageInfo
pkgInfo: PartialWrapPackageInfo,
constructorArgs: unknown[]
) {
// Disable instrumentation if event-based logging is used
// $extends() breaks $on() which is required for event-based logging
// See: https://github.com/prisma/prisma/issues/24070
if (this.usesEventBasedLogging(constructorArgs)) {
// eslint-disable-next-line no-console
console.warn(
"AIKIDO: Prisma instrumentation disabled because event-based logging (emit: 'event') is enabled. Zen uses $extends() internally which is incompatible with $on(). See: https://github.com/prisma/prisma/issues/24070"
);
return;
}

const isNoSQLClient = this.isNoSQLClient(instance);

const agent = getInstance();
Expand Down Expand Up @@ -242,8 +274,8 @@ export class Prisma implements Wrapper {
const accessLocalVariables: LocalVariableAccessConfig = {
names: ["module.exports"],
cb: (vars, pkgInfo) => {
wrapNewInstance(vars[0], "PrismaClient", pkgInfo, (instance) => {
return this.instrumentPrismaClient(instance, pkgInfo);
wrapNewInstance(vars[0], "PrismaClient", pkgInfo, (instance, args) => {
return this.instrumentPrismaClient(instance, pkgInfo, args);
});
},
};
Expand All @@ -252,8 +284,8 @@ export class Prisma implements Wrapper {
.addPackage("@prisma/client")
.withVersion("^5.0.0 || ^6.0.0")
.onRequire((exports, pkgInfo) => {
wrapNewInstance(exports, "PrismaClient", pkgInfo, (instance) => {
return this.instrumentPrismaClient(instance, pkgInfo);
wrapNewInstance(exports, "PrismaClient", pkgInfo, (instance, args) => {
return this.instrumentPrismaClient(instance, pkgInfo, args);
});
})
.addFileInstrumentation({
Expand Down
Loading