flawopen.com/SQL Injection/C/C++

SQL Injection in C/C++

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 C/C++, this class of bug frequently arrives paired with a second one: building the query with sprintf into a fixed-size buffer risks a buffer overflow (CWE-120) on the exact same line that risks SQL injection — two distinct vulnerability classes from one habit.

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 from network input */
char query[256];
sprintf(query,
  "SELECT * FROM users WHERE id = %s",
  user_id);
sqlite3_exec(db, query, cb, 0, &errmsg);
FIXED
/* value bound, never in the query text */
sqlite3_stmt *stmt;
sqlite3_prepare_v2(db,
  "SELECT * FROM users WHERE id = ?",
  -1, &stmt, 0);
sqlite3_bind_text(stmt, 1, user_id,
  -1, SQLITE_TRANSIENT);
sqlite3_step(stmt);

Why the fix works

sqlite3_prepare_v2 compiles the query's structure once; sqlite3_bind_text attaches the value to a placeholder afterward, entirely outside the SQL text the parser sees. sqlite3_exec has no equivalent mechanism at all — it runs a complete string as-is, so escaping is the only defense available to it, and escaping is not the same guarantee as parameter binding.

C/C++-specific gotchas

sqlite3_exec simply has no parameter-binding API

Switching away from the vulnerable pattern here isn't a matter of adding escaping to sqlite3_exec — that function only ever takes a complete SQL string. The fix requires switching to the sqlite3_prepare_v2 / sqlite3_bind_* / sqlite3_step API entirely.

Fixing the buffer overflow doesn't fix the injection

Switching from sprintf to snprintf closes the memory-safety hole but does nothing about the query-structure hole — they're independent bugs from the same line of code and both need fixing.

Other client libraries (MySQL C API, libpq) have their own bind functions

MySQL's C API uses mysql_stmt_bind_param; PostgreSQL's libpq uses PQexecParams. The specific function names differ by library, but the underlying principle — never build the query string with the value inline — is identical across all of them.

Common misconceptions

"I bounds-checked the buffer, so it's safe"

Buffer safety and query-structure safety are separate concerns. A perfectly bounds-checked snprintf call still produces an injectable query if the value is interpolated into it.

"This runs locally / isn't network-facing, so it's low risk"

Any value that ultimately traces back to something outside the process's own control — a file, an environment variable, another local process — counts as untrusted for this purpose, not just direct network input.

How to check if you're affected

grep -rn "sprintf(" --include="*.c" --include="*.cpp" . | grep -i "select\|insert\|update\|delete" grep -rn "sqlite3_exec(" --include="*.c" --include="*.cpp" .
Treat every sqlite3_exec call as a candidate for manual review — the function is legitimate for static, non-parameterized statements too, so a grep hit isn't automatically a bug, only a place to check.

Prevention checklist

FAQ

Is sqlite3_exec always dangerous?

No — it's safe for static SQL with no embedded user data. It becomes dangerous only when the query string it receives was built with user-controlled input.

Does this apply the same way to MySQL/Postgres C client libraries?

The principle is identical — the specific bind function names differ (mysql_stmt_bind_param, PQexecParams) but each library has its own parameterized-query mechanism to use instead of string building.

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