Skip to content
Navigation

Type at least two characters. Search covers page titles, headings, tags and database names.

↑ ↓ to navigateEnter to openEsc to close0 pages
Securityintermediate

Injection

How SQL and NoSQL injection actually happen, and the single technique that prevents both.

2 min readIntermediateUpdated Edit this page

Injection remains one of the most exploited classes of vulnerability, and the prevention has been known for decades: never build a query by concatenating untrusted input.

SQL Injection

What parameters cannot do

Parameters bind values. They cannot bind identifiers or SQL structure:

# Invalid: a table name is not a value.
cursor.execute("SELECT * FROM %s", (table_name,))

For dynamic identifiers or sort directions, validate against an allowlist:

ALLOWED_SORT = {"created_at", "total_cents", "status"}
if sort_column not in ALLOWED_SORT:
    raise ValueError("invalid sort column")
query = f"SELECT * FROM orders ORDER BY {sort_column} DESC LIMIT %s"
cursor.execute(query, (limit,))

The allowlist — not escaping — is what makes this safe.

ORMs are not automatic protection

Most ORMs parameterise by default, and most also provide a raw-SQL escape hatch that does not. Model.objects.raw(...), session.execute(text(...)) and query builders that accept a raw fragment are all injection points if they receive concatenated input. Review those specifically.

NoSQL Injection

The absence of SQL does not remove the vulnerability class; it changes its shape. In MongoDB the classic case is an operator injected where a value was expected:

// Vulnerable: a JSON body of {"email": {"$ne": null}, "password": {"$ne": null}}
// matches the first user in the collection.
db.users.findOne({ email: req.body.email, password: req.body.password });
// Safe: coerce to the expected type so an object cannot become an operator.
db.users.findOne({
  email: String(req.body.email),
  password: String(req.body.password)
});

Validate the request body against a schema so email must be a string before it reaches the query at all.

Redis deserves specific mention: commands are not string-parsed the way SQL is, but building a command from unvalidated input can still let an attacker read or overwrite keys outside the intended namespace. Validate key components, and constrain the account with an ACL key pattern.

Defence in depth

Parameterisation is the fix. These limit the damage when something is missed:

  • Least privilege. An account with SELECT, INSERT and UPDATE on the application schema cannot drop tables. See Authorization.
  • Row-level security, so a successful injection still cannot cross a tenant boundary.
  • Query timeouts and result limits, which bound a data exfiltration attempt.
  • Audit logging on sensitive tables, so a bulk read is visible.
  • Automated scanning in CI for concatenated query construction.