[{"data":1,"prerenderedAt":367},["ShallowReactive",2],{"blog-how-to/prisma-security":3},{"id":4,"title":5,"body":6,"category":348,"date":349,"dateModified":349,"description":350,"draft":351,"extension":352,"faq":353,"featured":351,"headerVariant":354,"image":353,"keywords":353,"meta":355,"navigation":356,"ogDescription":357,"ogTitle":353,"path":358,"readTime":353,"schemaOrg":359,"schemaType":360,"seo":361,"sitemap":362,"stem":363,"tags":364,"twitterCard":365,"__hash__":366},"blog/blog/how-to/prisma-security.md","How to Secure Prisma ORM",{"type":7,"value":8,"toc":332},"minimark",[9,13,17,21,27,30,43,48,51,55,78,94,107,120,133,146,159,189,193,221,227,231,236,243,247,254,258,265,269,272,294,313],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-secure-prisma-orm",[18,19,20],"p",{},"Security best practices for modern TypeScript applications",[22,23,24],"tldr",{},[18,25,26],{},"TL;DR (20 minutes):\nPrisma's query builder is safe from SQL injection by default. The danger is in raw queries: always use\n$queryRaw\nwith template literals, never\n$queryRawUnsafe\nwith user input. Add middleware for access control, validate all inputs with Zod, and never expose your full database schema to clients.",[18,28,29],{},"Prerequisites:",[31,32,33,37,40],"ul",{},[34,35,36],"li",{},"Prisma ORM installed in your project",[34,38,39],{},"Basic understanding of TypeScript",[34,41,42],{},"Familiarity with SQL injection risks",[44,45,47],"h2",{"id":46},"why-this-matters","Why This Matters",[18,49,50],{},"Prisma is one of the safest ORMs available - its query builder automatically prevents SQL injection. However, developers often introduce vulnerabilities through raw queries, improper access control, or data exposure. This guide covers the security pitfalls specific to Prisma.",[44,52,54],{"id":53},"step-by-step-guide","Step-by-Step Guide",[56,57,59,64,67],"step",{"number":58},"1",[60,61,63],"h3",{"id":62},"understand-prismas-built-in-safety","Understand Prisma's built-in safety",[18,65,66],{},"Prisma Client methods are safe from SQL injection:",[68,69,74],"pre",{"className":70,"code":72,"language":73},[71],"language-text","// SAFE - Prisma automatically parameterizes\nconst user = await prisma.user.findUnique({\n  where: { email: userInput }  // userInput is safely escaped\n});\n\nconst users = await prisma.user.findMany({\n  where: {\n    OR: [\n      { name: { contains: searchTerm } },\n      { email: { contains: searchTerm } }\n    ]\n  }\n});\n\n// Even complex queries are safe\nconst orders = await prisma.order.findMany({\n  where: {\n    userId: userId,\n    status: { in: ['pending', 'processing'] },\n    createdAt: { gte: new Date(startDate) }\n  }\n});\n","text",[75,76,72],"code",{"__ignoreMap":77},"",[56,79,81,85,88],{"number":80},"2",[60,82,84],{"id":83},"handle-raw-queries-safely","Handle raw queries safely",[18,86,87],{},"Raw queries are where vulnerabilities creep in:",[68,89,92],{"className":90,"code":91,"language":73},[71],"// DANGEROUS - SQL injection vulnerability!\nconst result = await prisma.$queryRawUnsafe(\n  `SELECT * FROM users WHERE email = '${userInput}'`\n);\n\n// SAFE - Use template literal with $queryRaw\nconst result = await prisma.$queryRaw`\n  SELECT * FROM users WHERE email = ${userInput}\n`;\n\n// SAFE - For dynamic queries, use Prisma.sql\nimport { Prisma } from '@prisma/client';\n\nconst columns = ['id', 'name', 'email'];\nconst safeColumns = columns\n  .filter(col => ['id', 'name', 'email'].includes(col))\n  .map(col => Prisma.raw(col));\n\nconst result = await prisma.$queryRaw`\n  SELECT ${Prisma.join(safeColumns)} FROM users\n  WHERE id = ${userId}\n`;\n\n// For dynamic table names - use allowlist\nconst ALLOWED_TABLES = ['users', 'posts', 'comments'];\nfunction getTable(tableName: string) {\n  if (!ALLOWED_TABLES.includes(tableName)) {\n    throw new Error('Invalid table name');\n  }\n  return Prisma.raw(tableName);\n}\n",[75,93,91],{"__ignoreMap":77},[56,95,97,101],{"number":96},"3",[60,98,100],{"id":99},"validate-inputs-before-queries","Validate inputs before queries",[68,102,105],{"className":103,"code":104,"language":73},[71],"import { z } from 'zod';\n\n// Define input schemas\nconst CreateUserSchema = z.object({\n  email: z.string().email().max(255),\n  name: z.string().min(1).max(100),\n  role: z.enum(['user', 'admin']).default('user')\n});\n\nconst UserIdSchema = z.string().uuid();\n\n// Validate in your API handlers\nasync function createUser(input: unknown) {\n  // Validate and sanitize input\n  const data = CreateUserSchema.parse(input);\n\n  // Now safe to use with Prisma\n  return prisma.user.create({ data });\n}\n\nasync function getUser(id: unknown) {\n  const userId = UserIdSchema.parse(id);\n  return prisma.user.findUnique({\n    where: { id: userId }\n  });\n}\n\n// Validate query parameters\nconst ListUsersSchema = z.object({\n  page: z.coerce.number().int().positive().default(1),\n  limit: z.coerce.number().int().min(1).max(100).default(20),\n  sortBy: z.enum(['createdAt', 'name', 'email']).default('createdAt'),\n  sortOrder: z.enum(['asc', 'desc']).default('desc')\n});\n",[75,106,104],{"__ignoreMap":77},[56,108,110,114],{"number":109},"4",[60,111,113],{"id":112},"implement-access-control-middleware","Implement access control middleware",[68,115,118],{"className":116,"code":117,"language":73},[71],"import { PrismaClient, Prisma } from '@prisma/client';\n\nconst prisma = new PrismaClient();\n\n// Middleware to enforce access control\nprisma.$use(async (params, next) => {\n  const context = getRequestContext(); // Your auth context\n\n  // Enforce user can only access their own data\n  if (params.model === 'Order') {\n    if (params.action === 'findMany' || params.action === 'findFirst') {\n      params.args.where = {\n        ...params.args.where,\n        userId: context.userId  // Always filter by current user\n      };\n    }\n    if (params.action === 'update' || params.action === 'delete') {\n      params.args.where = {\n        ...params.args.where,\n        userId: context.userId\n      };\n    }\n  }\n\n  // Prevent deletion of audit logs\n  if (params.model === 'AuditLog' && params.action === 'delete') {\n    throw new Error('Audit logs cannot be deleted');\n  }\n\n  return next(params);\n});\n\n// Or use Prisma Client Extensions (newer approach)\nconst prismaWithAuth = prisma.$extends({\n  query: {\n    order: {\n      async findMany({ args, query }) {\n        const ctx = getRequestContext();\n        args.where = { ...args.where, userId: ctx.userId };\n        return query(args);\n      }\n    }\n  }\n});\n",[75,119,117],{"__ignoreMap":77},[56,121,123,127],{"number":122},"5",[60,124,126],{"id":125},"control-field-exposure","Control field exposure",[68,128,131],{"className":129,"code":130,"language":73},[71],"// DON'T return entire user objects to clients\nasync function getUser(id: string) {\n  const user = await prisma.user.findUnique({\n    where: { id }\n  });\n  return user; // Exposes password hash, internal fields, etc.\n}\n\n// DO select only needed fields\nasync function getUser(id: string) {\n  return prisma.user.findUnique({\n    where: { id },\n    select: {\n      id: true,\n      name: true,\n      email: true,\n      createdAt: true\n      // password, internal fields excluded\n    }\n  });\n}\n\n// Create reusable select objects\nconst publicUserSelect = {\n  id: true,\n  name: true,\n  email: true,\n  avatar: true,\n  createdAt: true\n} satisfies Prisma.UserSelect;\n\nconst privateUserSelect = {\n  ...publicUserSelect,\n  role: true,\n  settings: true\n} satisfies Prisma.UserSelect;\n\n// Use in queries\nconst user = await prisma.user.findUnique({\n  where: { id },\n  select: isAdmin ? privateUserSelect : publicUserSelect\n});\n",[75,132,130],{"__ignoreMap":77},[56,134,136,140],{"number":135},"6",[60,137,139],{"id":138},"secure-connection-string-handling","Secure connection string handling",[68,141,144],{"className":142,"code":143,"language":73},[71],"// schema.prisma\ndatasource db {\n  provider = \"postgresql\"\n  url      = env(\"DATABASE_URL\")\n}\n\n// NEVER do this:\n// url = \"postgresql://user:password@host:5432/db\"\n\n// For serverless with connection pooling\ndatasource db {\n  provider  = \"postgresql\"\n  url       = env(\"DATABASE_URL\")       // Pooled connection\n  directUrl = env(\"DIRECT_DATABASE_URL\") // For migrations\n}\n\n// .env (local development only)\nDATABASE_URL=\"postgresql://...\"\n\n// Production: Set in your platform's environment variables\n// Never commit .env files with real credentials\n\n// Validate environment variables at startup\nimport { z } from 'zod';\n\nconst envSchema = z.object({\n  DATABASE_URL: z.string().url(),\n  NODE_ENV: z.enum(['development', 'production', 'test'])\n});\n\nenvSchema.parse(process.env);\n",[75,145,143],{"__ignoreMap":77},[56,147,149,153],{"number":148},"7",[60,150,152],{"id":151},"implement-query-limits-and-pagination","Implement query limits and pagination",[68,154,157],{"className":155,"code":156,"language":73},[71],"// DANGEROUS - could return millions of rows\nconst allUsers = await prisma.user.findMany();\n\n// SAFE - Always limit results\nconst users = await prisma.user.findMany({\n  take: 100,  // Maximum records to return\n  skip: 0     // Offset for pagination\n});\n\n// Implement cursor-based pagination (more efficient)\nasync function getUsers(cursor?: string, limit = 20) {\n  const users = await prisma.user.findMany({\n    take: limit + 1, // Fetch one extra to check if there's more\n    ...(cursor && {\n      cursor: { id: cursor },\n      skip: 1 // Skip the cursor itself\n    }),\n    orderBy: { createdAt: 'desc' }\n  });\n\n  const hasMore = users.length > limit;\n  const data = hasMore ? users.slice(0, -1) : users;\n  const nextCursor = hasMore ? data[data.length - 1].id : null;\n\n  return { data, nextCursor, hasMore };\n}\n\n// Enforce maximum limits\nconst MAX_LIMIT = 100;\nfunction getLimit(requested?: number): number {\n  if (!requested || requested \u003C 1) return 20;\n  return Math.min(requested, MAX_LIMIT);\n}\n",[75,158,156],{"__ignoreMap":77},[160,161,162,165],"warning-box",{},[18,163,164],{},"Common Prisma Security Mistakes:",[31,166,167,174,177,180,183,186],{},[34,168,169,170,173],{},"Using ",[75,171,172],{},"$queryRawUnsafe"," with any user input",[34,175,176],{},"Exposing internal fields (password hashes, tokens) in API responses",[34,178,179],{},"No pagination on findMany queries",[34,181,182],{},"Missing access control - users can access other users' data",[34,184,185],{},"Committing .env files with database credentials",[34,187,188],{},"Using the same database user for app and migrations",[44,190,192],{"id":191},"how-to-verify-it-worked","How to Verify It Worked",[194,195,196,203,209,215],"ol",{},[34,197,198,202],{},[199,200,201],"strong",{},"Test for SQL injection:"," Try malicious inputs in your API",[34,204,205,208],{},[199,206,207],{},"Test access control:"," Verify users can't access others' data",[34,210,211,214],{},[199,212,213],{},"Check field exposure:"," Review API responses for sensitive data",[34,216,217,220],{},[199,218,219],{},"Test pagination:"," Ensure large queries are limited",[68,222,225],{"className":223,"code":224,"language":73},[71],"// Test with malicious inputs\nconst testInputs = [\n  \"'; DROP TABLE users; --\",\n  \"1 OR 1=1\",\n  \"admin'--\",\n  \"1; SELECT * FROM users\"\n];\n\nfor (const input of testInputs) {\n  const result = await api.searchUsers({ query: input });\n  // Should return empty results, not errors or all data\n}\n",[75,226,224],{"__ignoreMap":77},[44,228,230],{"id":229},"common-errors-troubleshooting","Common Errors & Troubleshooting",[232,233,235],"h4",{"id":234},"raw-query-returning-unexpected-results","Raw query returning unexpected results",[18,237,238,239,242],{},"Check if you're using template literals correctly. ",[75,240,241],{},"$queryRaw"," with backticks parameterizes; string concatenation doesn't.",[232,244,246],{"id":245},"middleware-not-running","Middleware not running",[18,248,249,250,253],{},"Ensure middleware is registered before queries. Use ",[75,251,252],{},"$use"," immediately after creating PrismaClient.",[232,255,257],{"id":256},"cannot-read-property-where-of-undefined","\"Cannot read property 'where' of undefined\"",[18,259,260,261,264],{},"Some operations don't have args. Check ",[75,262,263],{},"params.args"," exists before modifying.",[232,266,268],{"id":267},"select-and-include-conflict","Select and include conflict",[18,270,271],{},"You can't use both select and include. Choose one or restructure your query.",[273,274,275,282,288],"faq-section",{},[276,277,279],"faq-item",{"question":278},"Is Prisma safe from SQL injection by default?",[18,280,281],{},"Yes, for all Prisma Client methods (findMany, create, update, etc.). Only raw queries ($queryRaw, $executeRaw) require care - always use template literals, never string concatenation.",[276,283,285],{"question":284},"Should I use Prisma middleware or Client Extensions?",[18,286,287],{},"Client Extensions (introduced in Prisma 4.16) are the newer, more type-safe approach. Use them for new projects. Middleware still works and is fine for existing projects.",[276,289,291],{"question":290},"How do I implement row-level security with Prisma?",[18,292,293],{},"Use middleware to automatically add user filters to queries. For stronger guarantees, implement RLS in your database (PostgreSQL) and use Prisma with a user-scoped connection.",[18,295,296,299,304,305,304,309],{},[199,297,298],{},"Related guides:",[300,301,303],"a",{"href":302},"/blog/how-to/drizzle-security","Drizzle Security"," ·\n",[300,306,308],{"href":307},"/blog/how-to/parameterized-queries","Parameterized Queries",[300,310,312],{"href":311},"/blog/how-to/zod-validation","Zod Validation",[314,315,316,322,327],"related-articles",{},[317,318],"related-card",{"description":319,"href":320,"title":321},"Step-by-step guide to implementing CSRF protection in Next.js and Express. Token-based protection, SameSite cookies, and","/blog/how-to/implement-csrf-protection","How to Implement CSRF Protection",[317,323],{"description":324,"href":325,"title":326},"Step-by-step guide to implementing rate limiting. Protect your API from abuse with Upstash, Redis, or in-memory solution","/blog/how-to/implement-rate-limiting","How to Implement Rate Limiting in Your API",[317,328],{"description":329,"href":330,"title":331},"Step-by-step guide to secure JWT implementation. Choose the right algorithm, handle token storage, implement refresh tok","/blog/how-to/jwt-security","How to Implement JWT Security",{"title":77,"searchDepth":333,"depth":333,"links":334},2,[335,336,346,347],{"id":46,"depth":333,"text":47},{"id":53,"depth":333,"text":54,"children":337},[338,340,341,342,343,344,345],{"id":62,"depth":339,"text":63},3,{"id":83,"depth":339,"text":84},{"id":99,"depth":339,"text":100},{"id":112,"depth":339,"text":113},{"id":125,"depth":339,"text":126},{"id":138,"depth":339,"text":139},{"id":151,"depth":339,"text":152},{"id":191,"depth":333,"text":192},{"id":229,"depth":333,"text":230},"how-to","2026-01-20","Step-by-step guide to securing your Prisma ORM setup. Prevent injection attacks, handle raw queries safely, and implement proper access control.",false,"md",null,"yellow",{},true,"Security best practices for Prisma applications.","/blog/how-to/prisma-security","[object Object]","HowTo",{"title":5,"description":350},{"loc":358},"blog/how-to/prisma-security",[],"summary_large_image","OCuUvEJ7SPnNxsPWLr5fFgZU0CrnMHxs-AbBgxOnYuo",1775843928179]