SQL Injection Prevention Best Practices
SQL injection (SQLi) is a vulnerability where attacker-controlled input is interpreted as part of a SQL query, allowing unauthorized data access, modification, or administrative actions. Prevention is primarily a coding and configuration discipline: keep untrusted input separate from SQL syntax.
SQL injection prevention starts with one rule: use parameterized queries (prepared statements) everywhere and never concatenate user input into SQL strings. From there, you reduce blast radius with least-privilege database access, strict allowlists for unavoidable dynamic identifiers, and solid logging/testing so regressions don’t slip into production.
How SQL injection works
SQLi happens when an application mixes data and code in a database query. If user input is concatenated into a SQL string, an attacker can add characters (quotes, operators, comments) that change the query’s meaning.
Common failure pattern:
- App takes input (URL param, JSON field, form data).
- App builds a query string by concatenation.
- Database parses the resulting SQL and executes attacker-supplied fragments.
Example: vulnerable vs safe queries (practitioner view)
Vulnerable (string concatenation):
# DO NOT DO THIS
query = "SELECT * FROM users WHERE email = '" + email + "'"
cursor.execute(query)
Safe (parameterized query / prepared statement):
query = "SELECT * FROM users WHERE email = %s"
cursor.execute(query, (email,))
Parameterized queries work because the database driver sends the SQL template and the parameter values separately, so values cannot “break out” into SQL syntax.
SQL injection prevention best practices that actually work
1) Use parameterized queries everywhere (non-negotiable)
- Use parameters for
SELECT,INSERT,UPDATE,DELETE, and stored procedure calls. - Confirm your ORM/query builder is truly parameterizing (not interpolating strings).
- Make it easy to do the right thing: wrap database access behind a small set of approved helpers.
2) Avoid dynamic SQL; if unavoidable, use strict allowlists for identifiers
Dynamic SQL often appears in ORDER BY, dynamic column names, table names, or conditional filters.
You generally cannot parameterize identifiers (e.g., column names). Use allowlists:
// Example allowlist for ORDER BY column
const allowedSort = new Set(["name", "created_at", "status"]);
const sort = allowedSort.has(req.query.sort) ? req.query.sort : "created_at";
const sql = `SELECT id, name, status FROM users ORDER BY ${sort} DESC`; // identifier allowlist
// still parameterize values in WHERE clauses
Rules of thumb: - Allowlist only known-safe columns/tables; never pass through raw user strings. - Keep the allowlist close to the query (easy to review and test). - Treat “reporting” endpoints as high-risk: they’re often where dynamic SQL sneaks in.
3) Validate input, but don’t rely on validation as the primary defense
Input validation reduces risk and improves data quality, but it is not sufficient alone.
- Prefer allowlists (expected formats) over blocklists (trying to detect “bad” characters).
- Validate at boundaries (API gateways/controllers), but still parameterize in the data layer.
- Examples:
- Numeric IDs: enforce integer ranges
- UUIDs: strict UUID parsing
- Enums:
status ∈ {active, disabled, invited}
4) Enforce least privilege for database accounts (reduce blast radius)
Each service should use a DB user that only has the permissions it needs.
- Avoid using DBA/admin roles from application code.
- Split roles when feasible:
- read-only for read paths
- write-capable account only where mutations occur
- For multi-tenant apps, combine least privilege with strong tenant scoping and (where supported) row-level security.
PostgreSQL example (role with minimal privileges):
-- Create a login role for the app
CREATE ROLE app_user LOGIN PASSWORD 'use-a-secret-manager';
-- Grant only what the app needs
GRANT CONNECT ON DATABASE appdb TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
-- For future tables (adjust as needed)
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
5) Harden error handling (reduce information leakage)
- Don’t expose raw SQL errors to end users (they leak schema details and query structure).
- Return generic application errors; log detailed errors internally with correlation IDs.
- Ensure production error pages don’t echo query strings, stack traces, or driver exceptions.
6) Centralize query building and enforce it in code review
- Create a small number of data-access modules so engineers don’t hand-roll SQL in random endpoints.
- Use a review checklist item: “No string-built SQL (including f-strings/templates).”
- Add linting rules where possible (language-specific).
7) Add compensating controls for visibility (not as a replacement)
- A WAF can block noisy, common SQLi payloads and buy time, but it does not replace fixing the code.
- Where appropriate, consider runtime protection (RASP-style approaches) for additional telemetry and enforcement. If you’re evaluating RASP concepts, see: what is runtime application self protection.
Operationally, these controls help you: - spot probing early, - prioritize vulnerable endpoints, - and reduce opportunistic scanning impact.
8) Test and verify continuously (prevent regressions)
- Add unit tests for query helpers to ensure parameterization is used.
- Run DAST against staging to catch regressions.
- Use SAST and dependency scanning to keep database drivers/ORMs updated and to flag unsafe query patterns. For a quick refresher on dependency scanning, see: what is software composition analysis.
Technical notes: quick checks you can run
Find common risky patterns in code (ripgrep examples)
# Look for string concatenation around SQL keywords
rg -n --hidden --glob '!**/node_modules/**' "SELECT .*\\+|INSERT .*\\+|UPDATE .*\\+|DELETE .*\\+" .
# Look for common unsafe execution APIs (language-specific examples)
rg -n "execute\\(f\"|execute\\(\"\\s*\\+|query\\(\\s*\"\\s*\\+" .
Log patterns that can indicate SQLi probing
These aren’t definitive, but they’re useful signals in web logs:
%27 # URL-encoded single quote (')
%22 # URL-encoded double quote (")
%2D%2D # URL-encoded -- comment
%3B # URL-encoded semicolon ;
UNION+SELECT # UNION SELECT probes
OR+1%3D1 # OR 1=1 patterns
information_schema
pg_catalog
If you see these patterns concentrated on specific endpoints, prioritize review of their query construction and DB privileges.
Where you’ll encounter SQL injection risk in real systems
SQL injection prevention matters anywhere your application accepts input and talks to a relational database. Common hotspots:
- Login and password reset flows (
email,username,tokenparameters). - Search and filter endpoints with many optional query parameters.
- Admin panels that build reports dynamically (filters, sorting, grouping).
- Legacy apps using raw SQL strings or older database libraries.
- APIs receiving JSON (SQLi is not limited to forms; any input channel can be abused).
- Multi-tenant apps where an injection can become a cross-tenant data breach if row-level access controls are weak.
Operationally, you’ll often “encounter” SQLi during:
- A pentest or bug bounty report showing unexpected data access.
- WAF alerts about UNION SELECT probes.
- Database logs showing syntax errors spiking after a new release.
- Incident response where attackers leveraged a single vulnerable endpoint to dump tables.
Practical tooling note (credential hygiene)
While SQLi is primarily a coding issue, incident response often reveals compounding problems like shared credentials or weak secrets handling. Using a business password manager can help teams rotate database credentials and store connection secrets more safely; 1Password is a common option (Try 1Password →).
This article may contain affiliate links. We earn a commission on qualifying purchases at no extra cost to you.
Related terms
The primary defense; SQL template and values are sent separately.
Can reduce SQLi risk if used correctly; still vulnerable when developers interpolate raw SQL.
Not automatically safe; safe only if they use parameters and avoid dynamic SQL concatenation internally.
Restrict DB permissions to minimize impact if an injection occurs.
Helps detect/block common payloads; best used as a compensating control and visibility layer.
Industry list that includes injection risks; useful for governance and prioritization.
Often misunderstood; escaping is not a universal fix and is error-prone across DBs/encodings—prefer parameters.
Malicious input stored in the database and later used unsafely in a query; reinforces why all query construction must be safe, not just “public” endpoints.