@aikidosec/firewall
Version:
Zen by Aikido is an embedded Application Firewall that autonomously protects Node.js apps against common and critical attacks, provides rate limiting, detects malicious traffic (including bots), and more.
207 lines (206 loc) • 9.15 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Prisma = void 0;
const wrapNewInstance_1 = require("../agent/hooks/wrapNewInstance");
const SQLDialectMySQL_1 = require("../vulnerabilities/sql-injection/dialects/SQLDialectMySQL");
const SQLDialectGeneric_1 = require("../vulnerabilities/sql-injection/dialects/SQLDialectGeneric");
const SQLDialectPostgres_1 = require("../vulnerabilities/sql-injection/dialects/SQLDialectPostgres");
const SQLDialectSQLite_1 = require("../vulnerabilities/sql-injection/dialects/SQLDialectSQLite");
const checkContextForSqlInjection_1 = require("../vulnerabilities/sql-injection/checkContextForSqlInjection");
const Context_1 = require("../agent/Context");
const AgentSingleton_1 = require("../agent/AgentSingleton");
const detectNoSQLInjection_1 = require("../vulnerabilities/nosql-injection/detectNoSQLInjection");
const wrapExport_1 = require("../agent/hooks/wrapExport");
const isPlainObject_1 = require("../helpers/isPlainObject");
const extractSQLFromObject_1 = require("./prisma/extractSQLFromObject");
const NOSQL_OPERATIONS_WITH_FILTER = ["findRaw"];
const NOSQL_OPERATIONS_WITH_PIPELINE = ["aggregateRaw"];
const SQL_OPERATIONS_TO_PROTECT = [
"$queryRawUnsafe",
"$executeRawUnsafe",
"$queryRaw",
"$executeRaw",
];
class Prisma {
// Check if the prisma client is a NoSQL client
isNoSQLClient(clientInstance) {
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;
}
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.
getClientSQLDialect(clientInstance) {
// https://github.com/prisma/prisma/blob/559988a47e50b4d4655dc45b11ceb9b5c73ef053/packages/generator-helper/src/types.ts#L75
if (!clientInstance ||
typeof clientInstance !== "object" ||
!("_engineConfig" in clientInstance) ||
!clientInstance._engineConfig ||
typeof clientInstance._engineConfig !== "object" ||
!("activeProvider" in clientInstance._engineConfig) ||
typeof clientInstance._engineConfig.activeProvider !== "string") {
return new SQLDialectGeneric_1.SQLDialectGeneric();
}
switch (clientInstance._engineConfig.activeProvider) {
case "mysql":
return new SQLDialectMySQL_1.SQLDialectMySQL();
case "postgresql":
case "postgres":
return new SQLDialectPostgres_1.SQLDialectPostgres();
case "sqlite":
return new SQLDialectSQLite_1.SQLDialectSQLite();
default:
return new SQLDialectGeneric_1.SQLDialectGeneric();
}
}
inspectSQLQuery(args, operation, dialect) {
const context = (0, Context_1.getContext)();
if (!context) {
return undefined;
}
const sql = (0, extractSQLFromObject_1.extractSQLFromObject)(args, dialect);
if (sql) {
return (0, checkContextForSqlInjection_1.checkContextForSqlInjection)({
sql: sql,
context: context,
operation: `prisma.${operation}`,
dialect: dialect,
});
}
return undefined;
}
inspectNoSQLQuery(args, operation, model) {
const context = (0, Context_1.getContext)();
if (!context) {
return undefined;
}
if (!args || typeof args !== "object") {
return undefined;
}
let filter;
if (NOSQL_OPERATIONS_WITH_FILTER.includes(operation) &&
"filter" in args) {
filter = args.filter;
}
if (NOSQL_OPERATIONS_WITH_PIPELINE.includes(operation) &&
"pipeline" in args) {
filter = args.pipeline;
}
if (filter) {
return this.inspectNoSQLFilter(model !== null && model !== void 0 ? model : "", context, filter, operation);
}
return undefined;
}
inspectNoSQLFilter(collection, request, filter, operation) {
const result = (0, detectNoSQLInjection_1.detectNoSQLInjection)(request, filter);
if (result.injection) {
return {
operation: `prisma.${operation}`,
kind: "nosql_injection",
source: result.source,
pathsToPayload: result.pathsToPayload,
metadata: {
collection: collection,
operation: operation,
filter: JSON.stringify(filter),
},
payload: result.payload,
};
}
}
onClientOperation({ model, operation, args, query, isNoSQLClient, sqlDialect, agent, pkgInfo, }) {
(0, wrapExport_1.inspectArgs)(args, () => {
if (isNoSQLClient) {
return this.inspectNoSQLQuery(args, operation, model);
}
if (SQL_OPERATIONS_TO_PROTECT.includes(operation)) {
return this.inspectSQLQuery(args, operation, sqlDialect || new SQLDialectGeneric_1.SQLDialectGeneric());
}
}, (0, Context_1.getContext)(), agent, pkgInfo, operation, isNoSQLClient ? "nosql_op" : "sql_op");
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
// protect clients against (No)SQL injections that use event-based logging.
// See: https://github.com/prisma/prisma/issues/24070
usesEventBasedLogging(constructorArgs) {
if (constructorArgs.length === 0) {
return false;
}
const options = constructorArgs[0];
if (!(0, isPlainObject_1.isPlainObject)(options) || !Array.isArray(options.log)) {
return false;
}
return options.log.some((entry) => (0, isPlainObject_1.isPlainObject)(entry) && entry.emit === "event");
}
instrumentPrismaClient(instance, pkgInfo, constructorArgs) {
// Disable (No)SQL injection protection 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)) {
// oxlint-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 = (0, AgentSingleton_1.getInstance)();
if (!agent) {
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: {
$allOperations: ({ model, operation, args, query, }) => {
return this.onClientOperation({
model,
operation,
args,
query,
isNoSQLClient,
sqlDialect: !isNoSQLClient
? this.getClientSQLDialect(instance)
: undefined,
agent,
pkgInfo,
});
},
},
});
}
wrap(hooks) {
const accessLocalVariables = {
names: ["module.exports"],
cb: (vars, pkgInfo) => {
(0, wrapNewInstance_1.wrapNewInstance)(vars[0], "PrismaClient", pkgInfo, (instance, args) => {
return this.instrumentPrismaClient(instance, pkgInfo, args);
});
},
};
hooks
.addPackage("@prisma/client")
.withVersion("^5.0.0 || ^6.0.0")
.onRequire((exports, pkgInfo) => {
(0, wrapNewInstance_1.wrapNewInstance)(exports, "PrismaClient", pkgInfo, (instance, args) => {
return this.instrumentPrismaClient(instance, pkgInfo, args);
});
})
.addFileInstrumentation({
path: "./default.js",
functions: [],
accessLocalVariables,
})
.addFileInstrumentation({
path: "./index.js",
functions: [],
accessLocalVariables,
});
}
}
exports.Prisma = Prisma;