← Back to the Journal
Injection · All engines

Preventing SQL Injection: Defense in Depth

AI-authored · reviewed by Connect·IT DBAs ·Jun 27, 2026·8 min read

SQL injection has sat near the top of the OWASP risk list for two decades, not because the fix is unknown, but because one forgotten string concatenation is all an attacker needs. Parameterized queries stop the vast majority of it — but a resilient system assumes that one layer will eventually fail, and puts several more behind it.

How injection actually happens

The root cause is always the same: untrusted input is concatenated into a query string, so data is interpreted as code. This is the classic mistake:

-- DANGEROUS: user input glued straight into SQL
query = "SELECT * FROM users WHERE email = '" + input + "'";
-- input of:  ' OR '1'='1
-- becomes:   SELECT * FROM users WHERE email = '' OR '1'='1'

The database has no way to know the attacker's quote was meant as data. Every defense below exists to remove that ambiguity or contain the blast radius when something slips through.

Layer 1: Parameterized queries (non-negotiable)

Bound parameters send the query structure and the data on separate channels, so input can never change the parsed statement. This is the single most important control:

-- Safe: the driver binds the value, never the SQL
SELECT * FROM users WHERE email = $1;   -- PostgreSQL
SELECT * FROM users WHERE email = ?;    -- MySQL / SQL Server
If you take one thing away: never build SQL with string concatenation. Use bound parameters or a query builder / ORM that does it for you — for every query, including the "internal" ones.

Layer 2: Least-privilege database accounts

The application should connect with an account that can do exactly what the app needs and nothing more. If an injection does land, least privilege decides whether it reads one table or drops the schema.

  • No DROP, ALTER, or GRANT for the app role.
  • Separate read-only and read-write roles where the workload allows it.
  • Never let the application connect as a superuser or db_owner.

Layer 3: Validate and constrain input

Parameterization protects values, but identifiers (table names, column names, ORDER BY targets) can't be bound. For those, use an allow-list — never pass user input through directly:

-- Map user choice to a fixed set; reject anything else
const sortable = { name: "name", date: "created_at" };
const column = sortable[userInput] ?? "created_at";

Layer 4: Detect and monitor

Assume something will eventually get through and make sure you'd see it. A web application firewall catches common payloads at the edge, and database-level monitoring catches the anomalies a WAF misses.

  • Alert on query errors spiking — injection probing is noisy.
  • Log and review queries that return abnormally large result sets.
  • Watch for syntax errors originating from application accounts.

The defense-in-depth checklist

  • Parameterize every query — no exceptions, no "trusted" inputs.
  • Allow-list identifiers that can't be parameterized.
  • Least-privilege roles so a breach is contained.
  • WAF + database monitoring to detect probing early.
  • Keep drivers and ORMs patched — injection bugs surface in libraries too.

No single layer is sufficient on its own, but stacked together they turn SQL injection from a catastrophic risk into a contained, observable one.

Tags Injection All engines Databases

About this article

Drafted by Connect·IT's AI authoring agents and reviewed by our senior DBAs before publishing. Want a security review of your data layer? Talk to our team →