flawopen.com/SQL Injection/Rust

SQL Injection in Rust

Critical CWE-89 Draft — pending review
ELI5

Imagine a form that only ever expects a ticket number, like 482. SQL injection is what happens when someone types something sneaky into that box instead of a number — a trick phrase that makes the system say "show me every ticket" instead of just ticket 482, because the system never checked that what it received was actually just a number.

Key terms on this page
user-controlled input
Any value that ultimately came from whoever is using — or attacking — the app: a form field, a URL parameter, an uploaded filename, an HTTP header. The app can't assume it's well-formed or safe.
SQL query
The command sent to a database — e.g. "get this row," "delete this table." Its meaning comes entirely from its exact text, which is what makes injecting extra text into it dangerous.

What's happening

SQL injection happens when user-controlled input gets inserted directly into a database query's text. In Rust, the trap is the format! macro — the idiomatic, safe-looking way to build almost any string in the language — used to build a query instead of passing the value through the database crate's own parameter binding.

Real-world impact

In 2015, UK telecom TalkTalk suffered a breach affecting over 150,000 customers after attackers exploited a SQL injection flaw in a legacy web page inherited through a company acquisition. The UK's data protection regulator fined TalkTalk £400,000, describing the failure as preventable and basic.

Source: UK Information Commissioner's Office enforcement notice, 2016 — see References below.

Vulnerable vs. fixed

VULNERABLE
// user_id comes straight from the request
let query = format!(
  "SELECT * FROM users WHERE id = {}",
  user_id
);
conn.execute(&query, [])?;
FIXED
// value bound, never formatted into the query
conn.execute(
  "SELECT * FROM users WHERE id = ?1",
  params![user_id],
)?;

Why the fix works

The placeholder ?1 tells the database crate to bind user_id as a separate parameter at the protocol level — the query's structure is fixed before the value is attached. format! can't provide this: it's pure string construction with no awareness that its output will be interpreted as SQL.

Rust-specific gotchas

format! is the exact trap pattern seen in other languages' native interpolation

Just as Python's f-strings and Go's Sprintf are idiomatic everywhere else in those languages, format! is Rust's default reach for string building — nothing about its syntax signals when it's being used to build something dangerous.

sqlx's compile-time query checking only protects parameterized queries

sqlx::query! validates a query's shape against your actual database schema at compile time — a genuinely distinctive Rust feature — but this check only runs against the placeholders you give it. Building the SQL string with format! first and passing the result to a plain query() call sidesteps that protection entirely.

Rust's memory safety doesn't extend to query semantics

Rust's ownership and borrow checker guarantee memory safety at compile time — they say nothing about whether a runtime string happens to be a syntactically valid, attacker-controlled SQL fragment. These are unrelated properties enforced by unrelated mechanisms.

Common misconceptions

"Rust prevents this class of bug by design"

Rust's safety guarantees are about memory (no null pointers, no data races, no buffer overflows) — SQL injection is a logic bug about string construction, a category Rust's type system doesn't address by default.

"sqlx's compile-time checks mean my queries are always safe"

They validate that a parameterized query matches your schema — they don't retroactively make an unparameterized, format!-built query safe.

How to check if you're affected

grep -rn "format!(" --include="*.rs" . | grep -i "select\|insert\|update\|delete" grep -rn "conn.execute(&query\|conn.query(&query" --include="*.rs" .
Clippy's clippy::format_push_string and general SQL-adjacent lints don't catch this specific pattern — a dedicated CI grep or a custom lint rule is currently the practical check.

Prevention checklist

FAQ

Does Rust's type system prevent SQL injection?

No — a String that happens to contain an injected SQL fragment is still a perfectly valid, well-typed String. Type safety and query-construction safety are independent properties.

Is diesel safer than rusqlite/sqlx here?

Diesel's query builder parameterizes by default, similar to an ORM in other languages. Its sql_query() raw-SQL escape hatch carries the same risk as any other language's raw-query method if built with format!.

References

View in: Python JavaScript Go Java PHP C# Ruby C/C++ Rust Kotlin Swift Solidity (N/A)
Also see: Command InjectionPath Traversal XSSInsecure Deserialization