Safeguard
Application Security•

How do you prevent SQL injection in C and C++?

Stop SQL injection in C and C++ by never building queries with snprintf. Use bind parameters in SQLite, libpq and similar APIs, and allow-list identifiers.

Safeguard Security Team
4 min read

You prevent SQL injection in C and C++ by keeping user input out of the query string. Write the SQL with placeholders, prepare it, and pass values through the driver's bind or parameter calls. Never build queries with sprintf, snprintf, strcat or std::string concatenation, even with a length check.

Why does C make this easy to get wrong?

C has no query builder in the standard library. The quickest thing to write is a format string, and it compiles and passes a happy path test. The bug only shows when someone types a quote.

Here is a constructed example, a lookup in a small inventory tool backed by SQLite:

char sql[256];
snprintf(sql, sizeof sql,
         "SELECT qty FROM stock WHERE sku = '%s'", sku_from_request);
sqlite3_exec(db, sql, on_row, NULL, &err);

If sku_from_request contains a quote followed by SQL, the quote ends the string literal and the rest is parsed as SQL. The buffer size check is a separate defence against memory bugs. It does nothing about meaning. In fact, silent truncation by snprintf can change what a query does, which is its own problem.

What does the safe version look like?

With SQLite you prepare the statement once and bind each value. The SQLite documentation describes parameters in the statement text using templates such as ?, ?NNN, :VVV, @VVV and $VVV, set with the sqlite3_bind_* routines. The leftmost parameter has index 1.

sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(db,
      "SELECT qty FROM stock WHERE sku = ?1", -1, &st, NULL) != SQLITE_OK) {
    /* handle error */
}
sqlite3_bind_text(st, 1, sku_from_request, -1, SQLITE_TRANSIENT);
while (sqlite3_step(st) == SQLITE_ROW) {
    int qty = sqlite3_column_int(st, 0);
}
sqlite3_finalize(st);

The value now travels as data. Whatever characters it contains, it cannot change the shape of the statement.

PostgreSQL's libpq has the same idea. PQexecParams lets you pass parameter values separately from the command text. The PostgreSQL manual notes that this avoids the need for tedious and error-prone quoting and escaping, and that it allows at most one SQL command in the string, which it calls a useful extra defence. Use $1, $2 placeholders and hand the values over in the paramValues array.

For C++, the same rules apply to whatever wrapper you use. A library that offers a query("... " + name) style call is just a nicer way to write the same bug. Look for the wrapper's bind or parameter method and ban string concatenation in review.

What about table names and ORDER BY?

Placeholders only stand in for values. Table names, column names and sort direction cannot be bound. The OWASP SQL Injection Prevention Cheat Sheet says to use allow-list input validation or redesign the query for these cases.

In C that is a lookup table:

static const char *sort_cols[] = { "sku", "qty", "updated_at" };
const char *col = NULL;
for (size_t i = 0; i < 3; i++)
    if (strcmp(user_sort, sort_cols[i]) == 0) col = sort_cols[i];
if (!col) col = "sku";

You then splice only the string you picked from your own array. The user's text never enters the query.

Is escaping good enough?

The same cheat sheet lists escaping all user input as strongly discouraged. It is database specific, easy to get subtly wrong, and it cannot be guaranteed to hold across encodings and server settings. Treat escape functions as a last resort for legacy code you cannot yet change, and plan to replace them.

What to do

  1. Search the codebase for sprintf, snprintf, strcat, std::string concatenation and format libraries near words like SELECT, INSERT, UPDATE and DELETE.
  2. Replace each hit with a prepared statement and bound values.
  3. Move dynamic identifiers to allow-lists in code, never from the request.
  4. Give the application database account only the rights it needs, so a missed hit has less reach.
  5. Add a static analysis rule or a code review checklist line: no SQL built from variables.
  6. Test with a quote character and a comment marker in every field that reaches a query, and keep those as regression tests.

Safeguard's code scanning can flag string-built queries in a repository as one input to that review. It does not replace the fix.

Quick check

Does a length check with snprintf stop SQL injection?

No. It limits buffer size, which helps against memory corruption. It does not change how the database parses the text. Use bind parameters.

Can I bind a column name?

No. Placeholders are for values only. Map user choices to a fixed list of column names in your own code.

Sources

Last reviewed: 18 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.