[{"data":1,"prerenderedAt":475},["ShallowReactive",2],{"blog-best-practices/database":3},{"id":4,"title":5,"body":6,"category":449,"date":450,"dateModified":451,"description":452,"draft":453,"extension":454,"faq":455,"featured":453,"headerVariant":460,"image":461,"keywords":461,"meta":462,"navigation":463,"ogDescription":464,"ogTitle":461,"path":465,"readTime":466,"schemaOrg":467,"schemaType":468,"seo":469,"sitemap":470,"stem":471,"tags":472,"twitterCard":473,"__hash__":474},"blog/blog/best-practices/database.md","Database Security Best Practices: SQL Injection, Access Control, and Encryption",{"type":7,"value":8,"toc":436},"minimark",[9,16,25,30,33,48,58,62,65,74,126,130,133,142,146,149,158,163,182,186,189,198,202,205,209,229,233,236,245,249,321,350,378,382,385,405,424],[10,11,12],"tldr",{},[13,14,15],"p",{},"The #1 database security best practice is using parameterized queries. Never concatenate user input into SQL, use least-privilege access, encrypt sensitive data at rest, and secure your connection strings. These practices prevent 87% of database breaches.",[17,18,19],"quotable-box",{},[20,21,22],"blockquote",{},[13,23,24],{},"\"Your database is only as secure as its weakest query. One unparameterized input is all it takes for a complete breach.\"",[26,27,29],"h2",{"id":28},"best-practice-1-prevent-sql-injection-5-min","Best Practice 1: Prevent SQL Injection 5 min",[13,31,32],{},"SQL injection is one of the most common and dangerous vulnerabilities. Always use parameterized queries:",[34,35,37],"code-block",{"label":36},"SQL injection prevention",[38,39,44],"pre",{"className":40,"code":42,"language":43},[41],"language-text","// WRONG: SQL injection vulnerability\nconst query = `SELECT * FROM users WHERE id = ${userId}`;\nconst query2 = `SELECT * FROM users WHERE email = '${email}'`;\n\n// CORRECT: Parameterized query (node-postgres)\nconst result = await pool.query(\n  'SELECT * FROM users WHERE id = $1',\n  [userId]\n);\n\n// CORRECT: Using an ORM (Prisma)\nconst user = await prisma.user.findUnique({\n  where: { id: userId }\n});\n\n// CORRECT: Knex query builder\nconst users = await knex('users')\n  .where('email', email)\n  .first();\n","text",[45,46,42],"code",{"__ignoreMap":47},"",[49,50,51],"warning-box",{},[13,52,53,57],{},[54,55,56],"strong",{},"Never trust user input."," Even data that looks safe (numbers, emails) can contain SQL injection payloads. Always use parameterized queries or ORMs.",[26,59,61],{"id":60},"best-practice-2-use-least-privilege-access-4-min","Best Practice 2: Use Least-Privilege Access 4 min",[13,63,64],{},"Database users should only have the permissions they need:",[34,66,68],{"label":67},"PostgreSQL role setup",[38,69,72],{"className":70,"code":71,"language":43},[41],"-- Create a read-only role for the API\nCREATE ROLE api_readonly;\nGRANT CONNECT ON DATABASE myapp TO api_readonly;\nGRANT USAGE ON SCHEMA public TO api_readonly;\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO api_readonly;\n\n-- Create a role for normal app operations\nCREATE ROLE api_user;\nGRANT CONNECT ON DATABASE myapp TO api_user;\nGRANT USAGE ON SCHEMA public TO api_user;\nGRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO api_user;\n\n-- Create admin role (use sparingly)\nCREATE ROLE api_admin;\nGRANT ALL PRIVILEGES ON DATABASE myapp TO api_admin;\n\n-- Create users with specific roles\nCREATE USER app_service WITH PASSWORD 'secure_password';\nGRANT api_user TO app_service;\n\nCREATE USER reporting_service WITH PASSWORD 'secure_password';\nGRANT api_readonly TO reporting_service;\n",[45,73,71],{"__ignoreMap":47},[75,76,77,90],"table",{},[78,79,80],"thead",{},[81,82,83,87],"tr",{},[84,85,86],"th",{},"Use Case",[84,88,89],{},"Permissions Needed",[91,92,93,102,110,118],"tbody",{},[81,94,95,99],{},[96,97,98],"td",{},"Read-only API",[96,100,101],{},"SELECT only",[81,103,104,107],{},[96,105,106],{},"Standard app",[96,108,109],{},"SELECT, INSERT, UPDATE, DELETE",[81,111,112,115],{},[96,113,114],{},"Migrations",[96,116,117],{},"CREATE, ALTER, DROP (run separately)",[81,119,120,123],{},[96,121,122],{},"Admin tasks",[96,124,125],{},"Full access (use rarely)",[26,127,129],{"id":128},"best-practice-3-encrypt-sensitive-data-5-min","Best Practice 3: Encrypt Sensitive Data 5 min",[13,131,132],{},"Encrypt sensitive fields before storing them:",[34,134,136],{"label":135},"Field-level encryption",[38,137,140],{"className":138,"code":139,"language":43},[41],"import crypto from 'crypto';\n\nconst ENCRYPTION_KEY = process.env.ENCRYPTION_KEY; // 32 bytes\nconst IV_LENGTH = 16;\n\nfunction encrypt(text) {\n  const iv = crypto.randomBytes(IV_LENGTH);\n  const cipher = crypto.createCipheriv(\n    'aes-256-cbc',\n    Buffer.from(ENCRYPTION_KEY, 'hex'),\n    iv\n  );\n\n  let encrypted = cipher.update(text, 'utf8', 'hex');\n  encrypted += cipher.final('hex');\n\n  return iv.toString('hex') + ':' + encrypted;\n}\n\nfunction decrypt(encryptedText) {\n  const [ivHex, encrypted] = encryptedText.split(':');\n  const iv = Buffer.from(ivHex, 'hex');\n  const decipher = crypto.createDecipheriv(\n    'aes-256-cbc',\n    Buffer.from(ENCRYPTION_KEY, 'hex'),\n    iv\n  );\n\n  let decrypted = decipher.update(encrypted, 'hex', 'utf8');\n  decrypted += decipher.final('utf8');\n\n  return decrypted;\n}\n\n// Usage\nawait db.user.create({\n  data: {\n    email: email,\n    ssn: encrypt(ssn), // Encrypt sensitive fields\n  },\n});\n",[45,141,139],{"__ignoreMap":47},[26,143,145],{"id":144},"best-practice-4-secure-connection-strings-3-min","Best Practice 4: Secure Connection Strings 3 min",[13,147,148],{},"Database connection strings contain credentials. Handle them carefully:",[34,150,152],{"label":151},"Secure connection handling",[38,153,156],{"className":154,"code":155,"language":43},[41],"// Store in environment variables, never in code\nconst DATABASE_URL = process.env.DATABASE_URL;\n\n// For Prisma\n// In .env (never commit this file)\n// DATABASE_URL=\"postgresql://user:password@host:5432/db?sslmode=require\"\n\n// Validate connection string exists at startup\nif (!DATABASE_URL) {\n  console.error('DATABASE_URL is required');\n  process.exit(1);\n}\n\n// Use SSL in production\nconst pool = new Pool({\n  connectionString: DATABASE_URL,\n  ssl: process.env.NODE_ENV === 'production' ? {\n    rejectUnauthorized: true,\n    ca: process.env.DB_CA_CERT,\n  } : false,\n});\n",[45,157,155],{"__ignoreMap":47},[159,160,162],"h4",{"id":161},"connection-string-security","Connection String Security:",[164,165,166,170,173,176,179],"ul",{},[167,168,169],"li",{},"Never commit connection strings to version control",[167,171,172],{},"Use environment variables for credentials",[167,174,175],{},"Enable SSL for production connections",[167,177,178],{},"Rotate database passwords regularly",[167,180,181],{},"Use connection pooling to limit connections",[26,183,185],{"id":184},"best-practice-5-implement-row-level-security-4-min","Best Practice 5: Implement Row-Level Security 4 min",[13,187,188],{},"For multi-tenant apps, use database-level access controls:",[34,190,192],{"label":191},"PostgreSQL Row Level Security",[38,193,196],{"className":194,"code":195,"language":43},[41],"-- Enable RLS on table\nALTER TABLE user_data ENABLE ROW LEVEL SECURITY;\n\n-- Policy: users can only see their own data\nCREATE POLICY user_data_isolation ON user_data\n  FOR ALL\n  USING (user_id = current_setting('app.current_user_id')::uuid);\n\n-- In your application, set the user context\nawait pool.query(\"SET app.current_user_id = $1\", [userId]);\n\n-- Now queries automatically filter by user\nconst result = await pool.query('SELECT * FROM user_data');\n// Only returns data where user_id matches\n",[45,197,195],{"__ignoreMap":47},[26,199,201],{"id":200},"best-practice-6-backup-and-recovery-2-min","Best Practice 6: Backup and Recovery 2 min",[13,203,204],{},"Regular backups are essential for security and disaster recovery:",[159,206,208],{"id":207},"backup-security-checklist","Backup Security Checklist:",[164,210,211,214,217,220,223,226],{},[167,212,213],{},"Automate daily backups",[167,215,216],{},"Encrypt backups at rest",[167,218,219],{},"Store backups in a separate location",[167,221,222],{},"Test restore procedures regularly",[167,224,225],{},"Retain backups according to compliance requirements",[167,227,228],{},"Secure backup access with separate credentials",[26,230,232],{"id":231},"best-practice-7-audit-logging-4-min","Best Practice 7: Audit Logging 4 min",[13,234,235],{},"Track who accessed what data:",[34,237,239],{"label":238},"Audit logging setup",[38,240,243],{"className":241,"code":242,"language":43},[41],"-- Create audit log table\nCREATE TABLE audit_log (\n  id SERIAL PRIMARY KEY,\n  table_name TEXT NOT NULL,\n  action TEXT NOT NULL,\n  user_id UUID,\n  old_data JSONB,\n  new_data JSONB,\n  timestamp TIMESTAMPTZ DEFAULT NOW()\n);\n\n-- Audit trigger function\nCREATE OR REPLACE FUNCTION audit_trigger()\nRETURNS TRIGGER AS $$\nBEGIN\n  INSERT INTO audit_log (table_name, action, user_id, old_data, new_data)\n  VALUES (\n    TG_TABLE_NAME,\n    TG_OP,\n    current_setting('app.current_user_id', true)::uuid,\n    CASE WHEN TG_OP = 'DELETE' OR TG_OP = 'UPDATE'\n      THEN to_jsonb(OLD) ELSE NULL END,\n    CASE WHEN TG_OP = 'INSERT' OR TG_OP = 'UPDATE'\n      THEN to_jsonb(NEW) ELSE NULL END\n  );\n  RETURN COALESCE(NEW, OLD);\nEND;\n$$ LANGUAGE plpgsql;\n\n-- Apply to sensitive tables\nCREATE TRIGGER audit_users\n  AFTER INSERT OR UPDATE OR DELETE ON users\n  FOR EACH ROW EXECUTE FUNCTION audit_trigger();\n",[45,244,242],{"__ignoreMap":47},[26,246,248],{"id":247},"common-database-security-mistakes","Common Database Security Mistakes",[75,250,251,264],{},[78,252,253],{},[81,254,255,258,261],{},[84,256,257],{},"Mistake",[84,259,260],{},"Impact",[84,262,263],{},"Prevention",[91,265,266,277,288,299,310],{},[81,267,268,271,274],{},[96,269,270],{},"SQL string concatenation",[96,272,273],{},"SQL injection",[96,275,276],{},"Use parameterized queries",[81,278,279,282,285],{},[96,280,281],{},"Database as root user",[96,283,284],{},"Full system access if breached",[96,286,287],{},"Use least-privilege roles",[81,289,290,293,296],{},[96,291,292],{},"Unencrypted connections",[96,294,295],{},"Credential interception",[96,297,298],{},"Always use SSL",[81,300,301,304,307],{},[96,302,303],{},"Credentials in code",[96,305,306],{},"Exposed in version control",[96,308,309],{},"Use environment variables",[81,311,312,315,318],{},[96,313,314],{},"No backups",[96,316,317],{},"Data loss",[96,319,320],{},"Automate encrypted backups",[322,323,324],"info-box",{},[13,325,326,329,330,337,338,343,344,349],{},[54,327,328],{},"Official Resources:"," For comprehensive database security guidance, see ",[331,332,336],"a",{"href":333,"rel":334},"https://cheatsheetseries.owasp.org/cheatsheets/Database_Security_Cheat_Sheet.html",[335],"nofollow","OWASP Database Security Cheat Sheet",", ",[331,339,342],{"href":340,"rel":341},"https://cheatsheetseries.owasp.org/cheatsheets/Query_Parameterization_Cheat_Sheet.html",[335],"OWASP Query Parameterization Cheat Sheet",", and ",[331,345,348],{"href":346,"rel":347},"https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html",[335],"OWASP SQL Injection Prevention Cheat Sheet",".",[351,352,353,360,366,372],"faq-section",{},[354,355,357],"faq-item",{"question":356},"Do ORMs prevent SQL injection?",[13,358,359],{},"Yes, when used correctly. ORMs like Prisma, TypeORM, and Drizzle use parameterized queries internally. However, be careful with raw query methods that might allow string concatenation.",[354,361,363],{"question":362},"Should I encrypt all database fields?",[13,364,365],{},"No, encrypt only sensitive data like SSNs, payment info, and personal health data. Encryption adds complexity and prevents database-level searching. Use it strategically for high-sensitivity fields.",[354,367,369],{"question":368},"How do I secure a hosted database (RDS, Supabase)?",[13,370,371],{},"Use private subnets/VPCs when possible, enable SSL, use strong passwords, configure security groups to limit access, and use the platform's built-in encryption options.",[354,373,375],{"question":374},"What data should I audit?",[13,376,377],{},"Audit access to sensitive data (PII, financial), all admin actions, authentication events, and data modifications to critical tables. Balance thoroughness with storage and performance costs.",[26,379,381],{"id":380},"further-reading","Further Reading",[13,383,384],{},"Put these practices into action with our step-by-step guides.",[164,386,387,393,399],{},[167,388,389],{},[331,390,392],{"href":391},"/blog/how-to/add-security-headers","Add security headers to your app",[167,394,395],{},[331,396,398],{"href":397},"/blog/checklists/pre-deployment-security-checklist","Pre-deployment security checklist",[167,400,401],{},[331,402,404],{"href":403},"/blog/getting-started/first-scan","Run your first security scan",[406,407,408,414,419],"related-articles",{},[409,410],"related-card",{"description":411,"href":412,"title":413},"Supabase RLS and security","/blog/best-practices/supabase","Supabase Best Practices",[409,415],{"description":416,"href":417,"title":418},"Secure credential storage","/blog/best-practices/environment-variables","Environment Variables",[409,420],{"description":421,"href":422,"title":423},"Secure backup strategies","/blog/best-practices/backup","Backup Best Practices",[425,426,429,433],"cta-box",{"href":427,"label":428},"/","Start Free Scan",[26,430,432],{"id":431},"verify-your-database-security","Verify Your Database Security",[13,434,435],{},"Scan your application for database security issues.",{"title":47,"searchDepth":437,"depth":437,"links":438},2,[439,440,441,442,443,444,445,446,447,448],{"id":28,"depth":437,"text":29},{"id":60,"depth":437,"text":61},{"id":128,"depth":437,"text":129},{"id":144,"depth":437,"text":145},{"id":184,"depth":437,"text":185},{"id":200,"depth":437,"text":201},{"id":231,"depth":437,"text":232},{"id":247,"depth":437,"text":248},{"id":380,"depth":437,"text":381},{"id":431,"depth":437,"text":432},"best-practices","2026-01-22","2026-02-04","Essential database security best practices. Learn to prevent SQL injection, implement access controls, encrypt sensitive data, and secure your database connections.",false,"md",[456,457,458,459],{"question":356,"answer":359},{"question":362,"answer":365},{"question":368,"answer":371},{"question":374,"answer":377},"vibe-green",null,{},true,"Secure your database with injection prevention, access controls, and encryption.","/blog/best-practices/database","13 min read","[object Object]","Article",{"title":5,"description":452},{"loc":465},"blog/best-practices/database",[],"summary_large_image","zEimACQbmJ9I0dmGWKF9kluK2j3hVfV0IRHF2fAeaos",1775843918547]