[{"data":1,"prerenderedAt":361},["ShallowReactive",2],{"blog-how-to/parameterized-queries":3},{"id":4,"title":5,"body":6,"category":342,"date":343,"dateModified":343,"description":344,"draft":345,"extension":346,"faq":347,"featured":345,"headerVariant":348,"image":347,"keywords":347,"meta":349,"navigation":350,"ogDescription":351,"ogTitle":347,"path":352,"readTime":347,"schemaOrg":353,"schemaType":354,"seo":355,"sitemap":356,"stem":357,"tags":358,"twitterCard":359,"__hash__":360},"blog/blog/how-to/parameterized-queries.md","How to Use Parameterized Queries",{"type":7,"value":8,"toc":326},"minimark",[9,13,17,21,32,35,48,53,56,59,63,74,78,93,106,119,132,145,158,182,186,195,201,215,219,224,227,231,237,241,251,255,266,288,307],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-use-parameterized-queries",[18,19,20],"p",{},"The definitive defense against SQL injection attacks",[22,23,24],"tldr",{},[18,25,26,27,31],{},"TL;DR (15 minutes):\nNever concatenate user input into SQL strings. Use placeholders:\nSELECT * FROM users WHERE id = $1\nwith\n",[28,29,30],"span",{},"userId","\nas the parameter array. Every database driver supports this - it's the only reliable way to prevent SQL injection.",[18,33,34],{},"Prerequisites:",[36,37,38,42,45],"ul",{},[39,40,41],"li",{},"Basic SQL knowledge",[39,43,44],{},"A database (PostgreSQL, MySQL, SQLite, etc.)",[39,46,47],{},"Backend application code",[49,50,52],"h2",{"id":51},"why-this-matters","Why This Matters",[18,54,55],{},"SQL injection has been the #1 web vulnerability for over two decades. It allows attackers to read your entire database, modify data, delete tables, or even execute system commands. The fix is simple: parameterized queries.",[18,57,58],{},"AI code generators often create vulnerable string concatenation code. This guide shows you how to spot and fix these issues.",[49,60,62],{"id":61},"understanding-the-problem","Understanding the Problem",[64,65,70],"pre",{"className":66,"code":68,"language":69},[67],"language-text","// VULNERABLE - String concatenation\nconst query = `SELECT * FROM users WHERE email = '${userInput}'`;\n\n// If userInput is: ' OR '1'='1\n// Query becomes: SELECT * FROM users WHERE email = '' OR '1'='1'\n// This returns ALL users!\n\n// Even worse, if userInput is: '; DROP TABLE users; --\n// Query becomes: SELECT * FROM users WHERE email = ''; DROP TABLE users; --'\n// Your users table is gone.\n","text",[71,72,68],"code",{"__ignoreMap":73},"",[49,75,77],{"id":76},"step-by-step-guide","Step-by-Step Guide",[79,80,82,87],"step",{"number":81},"1",[83,84,86],"h3",{"id":85},"postgresql-with-node-postgres-pg","PostgreSQL with node-postgres (pg)",[64,88,91],{"className":89,"code":90,"language":69},[67],"import { Pool } from 'pg';\nconst pool = new Pool();\n\n// WRONG - Vulnerable to SQL injection\nasync function getUserBad(email) {\n  const result = await pool.query(\n    `SELECT * FROM users WHERE email = '${email}'`  // NEVER DO THIS\n  );\n  return result.rows[0];\n}\n\n// CORRECT - Parameterized query\nasync function getUser(email) {\n  const result = await pool.query(\n    'SELECT * FROM users WHERE email = $1',  // $1 is a placeholder\n    [email]  // Parameters passed separately\n  );\n  return result.rows[0];\n}\n\n// Multiple parameters\nasync function getUserByCredentials(email, status) {\n  const result = await pool.query(\n    'SELECT * FROM users WHERE email = $1 AND status = $2',\n    [email, status]  // $1 = email, $2 = status\n  );\n  return result.rows[0];\n}\n",[71,92,90],{"__ignoreMap":73},[79,94,96,100],{"number":95},"2",[83,97,99],{"id":98},"mysql-with-mysql2","MySQL with mysql2",[64,101,104],{"className":102,"code":103,"language":69},[67],"import mysql from 'mysql2/promise';\nconst pool = mysql.createPool({ /* config */ });\n\n// WRONG\nasync function getUserBad(email) {\n  const [rows] = await pool.execute(\n    `SELECT * FROM users WHERE email = '${email}'`\n  );\n  return rows[0];\n}\n\n// CORRECT - Use ? placeholders\nasync function getUser(email) {\n  const [rows] = await pool.execute(\n    'SELECT * FROM users WHERE email = ?',\n    [email]\n  );\n  return rows[0];\n}\n\n// Named placeholders (mysql2 feature)\nasync function getUser(email) {\n  const [rows] = await pool.execute(\n    'SELECT * FROM users WHERE email = :email',\n    { email }\n  );\n  return rows[0];\n}\n",[71,105,103],{"__ignoreMap":73},[79,107,109,113],{"number":108},"3",[83,110,112],{"id":111},"prisma-orm-already-safe","Prisma ORM (already safe)",[64,114,117],{"className":115,"code":116,"language":69},[67],"import { PrismaClient } from '@prisma/client';\nconst prisma = new PrismaClient();\n\n// Prisma automatically parameterizes - this is safe\nasync function getUser(email) {\n  return prisma.user.findUnique({\n    where: { email }  // Automatically parameterized\n  });\n}\n\n// Raw queries still need parameterization\nasync function searchUsers(searchTerm) {\n  // WRONG - vulnerable\n  return prisma.$queryRawUnsafe(\n    `SELECT * FROM users WHERE name LIKE '%${searchTerm}%'`\n  );\n\n  // CORRECT - use $queryRaw with template literal\n  return prisma.$queryRaw`\n    SELECT * FROM users WHERE name LIKE ${`%${searchTerm}%`}\n  `;\n}\n",[71,118,116],{"__ignoreMap":73},[79,120,122,126],{"number":121},"4",[83,123,125],{"id":124},"drizzle-orm","Drizzle ORM",[64,127,130],{"className":128,"code":129,"language":69},[67],"import { eq, like, sql } from 'drizzle-orm';\nimport { db } from './db';\nimport { users } from './schema';\n\n// Safe - Drizzle parameterizes automatically\nasync function getUser(email) {\n  return db.select().from(users).where(eq(users.email, email));\n}\n\n// For raw SQL, use sql template tag\nasync function searchUsers(searchTerm) {\n  return db.execute(\n    sql`SELECT * FROM users WHERE name LIKE ${`%${searchTerm}%`}`\n  );\n}\n",[71,131,129],{"__ignoreMap":73},[79,133,135,139],{"number":134},"5",[83,136,138],{"id":137},"mongodb-nosql-injection","MongoDB (NoSQL injection)",[64,140,143],{"className":141,"code":142,"language":69},[67],"import { MongoClient } from 'mongodb';\n\n// MongoDB has its own injection risks with operators\n// WRONG - allows operator injection\nasync function getUserBad(query) {\n  return collection.findOne(query);  // If query is { $gt: \"\" }, returns first user\n}\n\n// CORRECT - validate and sanitize\nasync function getUser(email) {\n  // Ensure email is a string, not an object with operators\n  if (typeof email !== 'string') {\n    throw new Error('Invalid email format');\n  }\n  return collection.findOne({ email });\n}\n\n// Or use strict typing\nasync function getUser(email: string) {\n  return collection.findOne({ email: { $eq: email } });\n}\n",[71,144,142],{"__ignoreMap":73},[79,146,148,152],{"number":147},"6",[83,149,151],{"id":150},"handle-dynamic-queries-safely","Handle dynamic queries safely",[64,153,156],{"className":154,"code":155,"language":69},[67],"// Sometimes you need dynamic column names or ORDER BY\n// These can't be parameterized - use allowlists instead\n\nconst ALLOWED_SORT_COLUMNS = ['created_at', 'name', 'email'];\nconst ALLOWED_SORT_ORDERS = ['ASC', 'DESC'];\n\nasync function getUsers(sortBy, sortOrder) {\n  // Validate against allowlist\n  if (!ALLOWED_SORT_COLUMNS.includes(sortBy)) {\n    throw new Error('Invalid sort column');\n  }\n  if (!ALLOWED_SORT_ORDERS.includes(sortOrder)) {\n    throw new Error('Invalid sort order');\n  }\n\n  // Now safe to use in query (still don't parameterize these)\n  const result = await pool.query(\n    `SELECT * FROM users ORDER BY ${sortBy} ${sortOrder}`\n  );\n  return result.rows;\n}\n\n// For dynamic WHERE conditions\nasync function searchUsers(filters) {\n  const conditions = [];\n  const params = [];\n  let paramIndex = 1;\n\n  if (filters.email) {\n    conditions.push(`email = $${paramIndex++}`);\n    params.push(filters.email);\n  }\n  if (filters.status) {\n    conditions.push(`status = $${paramIndex++}`);\n    params.push(filters.status);\n  }\n\n  const whereClause = conditions.length\n    ? `WHERE ${conditions.join(' AND ')}`\n    : '';\n\n  return pool.query(`SELECT * FROM users ${whereClause}`, params);\n}\n",[71,157,155],{"__ignoreMap":73},[159,160,161,164],"warning-box",{},[18,162,163],{},"Common Mistakes:",[36,165,166,173,176,179],{},[39,167,168,169,172],{},"Using template literals with SQL - ",[71,170,171],{},"SELECT * FROM users WHERE id = ${id}"," is still vulnerable",[39,174,175],{},"Parameterizing column names - use allowlists instead",[39,177,178],{},"Trusting \"internal\" data - always parameterize, even for data from other database queries",[39,180,181],{},"Using ORM raw query methods incorrectly - they often bypass the ORM's protections",[49,183,185],{"id":184},"how-to-verify-it-worked","How to Verify It Worked",[187,188,189],"ol",{},[39,190,191],{},[192,193,194],"strong",{},"Test with SQL injection payloads:",[64,196,199],{"className":197,"code":198,"language":69},[67],"// Test your endpoint with these inputs:\nconst testPayloads = [\n  \"' OR '1'='1\",\n  \"'; DROP TABLE users; --\",\n  \"1; SELECT * FROM users\",\n  \"admin'--\",\n  \"' UNION SELECT * FROM passwords--\"\n];\n\n// None of these should return unexpected data or cause errors\n// They should be treated as literal search strings\n",[71,200,198],{"__ignoreMap":73},[187,202,203,209],{},[39,204,205,208],{},[192,206,207],{},"Enable query logging:"," Check that parameters are being bound, not concatenated",[39,210,211,214],{},[192,212,213],{},"Use security scanners:"," Tools like SQLMap or OWASP ZAP can test for injection",[49,216,218],{"id":217},"common-errors-troubleshooting","Common Errors & Troubleshooting",[220,221,223],"h4",{"id":222},"error-bind-message-supplies-x-parameters-but-prepared-statement-requires-y","Error: \"bind message supplies X parameters, but prepared statement requires Y\"",[18,225,226],{},"Mismatch between placeholders and parameters. Count your $1, $2, etc. and make sure the array has the same number of elements.",[220,228,230],{"id":229},"error-could-not-determine-data-type-of-parameter-1","Error: \"could not determine data type of parameter $1\"",[18,232,233,234],{},"PostgreSQL can't infer the type. Cast explicitly: ",[71,235,236],{},"WHERE id = $1::integer",[220,238,240],{"id":239},"like-queries-not-working","LIKE queries not working",[18,242,243,244,247,248],{},"Include wildcards in the parameter, not the query: ",[71,245,246],{},"WHERE name LIKE $1"," with parameter ",[71,249,250],{},"%search%",[220,252,254],{"id":253},"in-clauses-with-dynamic-lists","IN clauses with dynamic lists",[18,256,257,258,261,262,265],{},"Generate placeholders dynamically: ",[71,259,260],{},"WHERE id IN ($1, $2, $3)"," based on array length, or use ",[71,263,264],{},"ANY($1)"," with an array parameter in PostgreSQL.",[267,268,269,276,282],"faq-section",{},[270,271,273],"faq-item",{"question":272},"Are ORMs automatically safe?",[18,274,275],{},"The query builder methods are usually safe. Raw query methods are not. Always check your ORM's documentation for their raw SQL escape patterns.",[270,277,279],{"question":278},"Should I also sanitize input if I use parameterized queries?",[18,280,281],{},"Parameterized queries handle SQL injection completely. Sanitization is for other concerns like XSS (output encoding) or business logic validation (email format, etc.).",[270,283,285],{"question":284},"What about stored procedures?",[18,286,287],{},"Stored procedures can still be vulnerable if they use dynamic SQL internally. Parameterize the call to the procedure AND make sure the procedure itself uses parameterized queries.",[18,289,290,293,298,299,298,303],{},[192,291,292],{},"Related guides:",[294,295,297],"a",{"href":296},"/blog/how-to/prevent-sql-injection","Prevent SQL Injection"," ·\n",[294,300,302],{"href":301},"/blog/how-to/prisma-security","Prisma Security",[294,304,306],{"href":305},"/blog/how-to/validate-user-input","Validate User Input",[308,309,310,316,321],"related-articles",{},[311,312],"related-card",{"description":313,"href":314,"title":315},"Step-by-step guide to securing Firebase with authentication-based security rules. Protect your Firestore and Realtime Da","/blog/how-to/firebase-auth-rules","How to Write Firebase Auth Rules",[311,317],{"description":318,"href":319,"title":320},"Step-by-step guide to setting up Firebase Authentication securely. Configure providers, integrate security rules, verify","/blog/how-to/firebase-auth","How to Set Up Firebase Auth Securely",[311,322],{"description":323,"href":324,"title":325},"Complete guide to Firebase Firestore and Realtime Database security rules. Learn rule syntax, common patterns, testing, ","/blog/how-to/firebase-security-rules","How to Write Firebase Security Rules",{"title":73,"searchDepth":327,"depth":327,"links":328},2,[329,330,331,340,341],{"id":51,"depth":327,"text":52},{"id":61,"depth":327,"text":62},{"id":76,"depth":327,"text":77,"children":332},[333,335,336,337,338,339],{"id":85,"depth":334,"text":86},3,{"id":98,"depth":334,"text":99},{"id":111,"depth":334,"text":112},{"id":124,"depth":334,"text":125},{"id":137,"depth":334,"text":138},{"id":150,"depth":334,"text":151},{"id":184,"depth":327,"text":185},{"id":217,"depth":327,"text":218},"how-to","2026-01-20","Step-by-step guide to using parameterized queries to prevent SQL injection. Examples for PostgreSQL, MySQL, MongoDB, and popular ORMs.",false,"md",null,"yellow",{},true,"Prevent SQL injection with parameterized queries - examples for every database.","/blog/how-to/parameterized-queries","[object Object]","HowTo",{"title":5,"description":344},{"loc":352},"blog/how-to/parameterized-queries",[],"summary_large_image","sStcvHZn8o4fx6e-83RedkSnnF-oKpFsuIzGW4SkUXc",1775843928143]