Safeguard
Security Guides•

Django raw SQL: safe parameters versus string formatting

Django QuerySets are parameterized, but raw(), cursor.execute(), extra() and RawSQL are not safe by default. Here is the one rule that keeps them safe.

Safeguard Security Team
3 min read

Django QuerySets are safe from SQL injection because the database driver escapes the parameters. Raw SQL is where it goes wrong. The rule is simple: pass user values as separate parameters, never build the SQL string with formatting or f-strings.

What is safe by default

According to the Django security docs, queries built through the ORM are parameterized. You write Person.objects.filter(last_name=user_input) and the driver handles escaping. You do not need to think about quoting.

Where the risk starts

The docs name the escape hatches: raw(), cursor.execute(), extra() and RawSQL. Use them sparingly, and always pass user-controlled values as parameters.

# Safe: parameters passed separately
Person.objects.raw("SELECT * FROM app_person WHERE last_name = %s", [user_input])

# Unsafe: string interpolation
Person.objects.raw(f"SELECT * FROM app_person WHERE last_name = '{user_input}'")

Notice that the safe form uses %s as a placeholder, not Python string formatting. The list at the end is what carries the value to the driver.

Why formatting is dangerous even when it looks harmless

An f-string pastes the value into the SQL text before the database parses it. A value containing a quote can end your string literal and add its own SQL. A parameter, by contrast, is sent as data and never parsed as SQL.

Placeholders cannot do everything

Placeholders work for values. They do not work for table names, column names or sort direction. If a user picks a sort column, map the input to a fixed allowlist in your code and put only your own constant into the SQL.

ALLOWED_SORT = {"name": "last_name", "created": "created_at"}
column = ALLOWED_SORT.get(request.GET.get("sort"), "created_at")

A review habit that catches most cases

Search the codebase for raw(, .extra(, RawSQL and cursor.execute. For each hit, ask two questions: does any value come from a request, and is it passed as a parameter. If a value is inside the SQL string, fix it.

Dependency scanning is a separate layer. A flaw in a library you did not write cannot be found by reading your own raw queries, and Safeguard's SBOM and dependency scanning covers that side. The free scan is at gold.safeguard.sh/scan.

Quick check

Is the ORM always safe from SQL injection?

Standard QuerySet use is parameterized. The risk comes from raw SQL features and from code that passes user input into them unsafely.

Can I use a placeholder for a column name?

No. Placeholders are for values. Map the input to an allowlist of column names you control.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.