Known since 1996. Still #3 on OWASP Top 10. Why the oldest vulnerability class can become a zero-day any day — through data flow blind spots, bad practices, and LLM-generated code.
BD-SECURITY-TDSQL — Parasoft C/C++testDetects possible SQL injections when tainted data reaches functions that prepare or execute SQL queries. Supported APIs: ODBC, ADO, OLE DB.
When an application uses unverified user data to construct SQL queries, an attacker can alter the SQL statements — taking total control of the database or executing system commands.
Static analysis tracks data from sources through propagators to sinks. No sanitizer on the path = vulnerability.
| Taint Sources | Propagators | Sinks | Sanitizers |
|---|---|---|---|
| Files, Pipes, Console | strcpy, strcat | SQLExecDirect | SQLBindParameter |
| Network, Sockets | sprintf, snprintf | SQLExecute | Custom validators |
| Environment Variables | Assignment, returns | mysql_query | Prepared statements |
| Entry-point params | Container insertions | PQexec | Parameterized queries |
Credentials read from a file concatenated directly into SQL. Tainted data flows from fread() → SQLExecDirect().
VULNERABLE 1#include <sql.h> 2#include <stdio.h> 3 4const char* reqBegin = "SELECT user_id, user_class, rights FROM users WHERE user_name = '"; 5const char* reqPass = "' and password = '"; 6const char* reqEnd = "'"; 7 8void handleRequest(FILE* file) { 9 char params[1000]; 10 fread(params, 1, 1000, file); // ← SOURCE 11 SQLCHAR req[1000]; 12 strcpy(req, reqBegin); 13 strcat(req, extractUsername(params)); // ← PROPAGATION 14 strcat(req, reqPass); 15 strcat(req, extractPassword(params)); // ← PROPAGATION 16 strcat(req, reqEnd); 17 SQLExecDirect(stmtHandle, req, strlen(req)); // ← SINK 💀 18}
' or ''=' → WHERE user_name = '' or ''='' and password = '' or ''=''
Using SQLPrepare() + SQLBindParameter() separates data from query structure — taint is neutralized.
SAFE 1const char* reqStr = "SELECT user_id, user_class, rights FROM users WHERE user_name = '?' and password = '?'"; 2 3void handleRequest(FILE* file) { 4 char params[1000]; 5 fread(params, 1, 1000, file); 6 char* name = extractUsername(params); 7 char* pass = extractPassword(params); 8 9 SQLBindParameter(stmtHandle, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, 20, 0, name, 0, &nameLen); 10 SQLBindParameter(stmtHandle, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, 20, 0, pass, 0, &passLen); 11 12 SQLPrepare(stmtHandle, reqStr, SQL_NTS); // ← SANITIZED 13 SQLExecute(stmtHandle); // ← SAFE 14}
A trusted db_fetch() helper looks safe — but taint flows through 4 functions across 2 files. A new caller creates a zero-day without changing existing code.
db_helpers.c — VULNERABLE 1// Developer assumes callers will sanitize — they don't. 2int db_fetch(const char* table, const char* where_clause, 3 SQLCHAR* result, SQLLEN maxlen) { 4 SQLCHAR query[2048]; 5 snprintf((char*)query, sizeof(query), 6 "SELECT * FROM %s WHERE %s", table, where_clause); // ← taint in 7 SQLExecDirect(g_stmt, query, SQL_NTS); // ← SINK 💀 8 // ... 9}
request_handler.c — VULNERABLE 1void handle_product_lookup(int sock) { 2 char buf[4096]; 3 recv(sock, buf, sizeof(buf)-1, 0); // ← SOURCE 4 buf[sizeof(buf)-1] = '\0'; 5 char* pid = parse_json_field(buf, "product_id"); // ← taint extracted 6 char* clause = build_filter(pid); // ← taint wrapped 7 SQLCHAR result[1024]; 8 db_fetch("products", clause, result, sizeof(result));// ← reaches sink 💀 9}
| Step | Variable | Status | Location |
|---|---|---|---|
| 1 | buf | TAINTED | request_handler.c:3 |
| 2 | pid | TAINTED | request_handler.c:5 |
| 3 | clause | TAINTED | request_handler.c:6 |
| 4 | query | TAINTED | db_helpers.c:6 |
| 5 | SQLExecDirect | SINK 💀 | db_helpers.c:7 |
db_helpers.c — SAFE 1int db_fetch_by_id(const char* table, const char* column, 2 const char* value, SQLCHAR* result, SQLLEN maxlen) { 3 SQLCHAR query[2048]; 4 snprintf((char*)query, sizeof(query), 5 "SELECT * FROM %s WHERE %s = ?", table, column); 6 SQLPrepare(g_stmt, query, SQL_NTS); 7 SQLLEN len = SQL_NTS; 8 SQLBindParameter(g_stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, 9 SQL_CHAR, 256, 0, (SQLPOINTER)value, 0, &len); A SQLExecute(g_stmt); // ← taint neutralized ✓ B // ... C}
db_fetch() that passes network data creates a new flow path — a new zero-day — even if db_helpers.c hasn't changed. The vulnerability emerges from relationships between components.
💬 "Write a C function that searches products by name from a CGI query string."
AI-GENERATED CODE 1/* search_products() — AI-generated, developer approved "looks good" */ 2char* get_cgi_param(const char* name) { 3 const char* qs = getenv("QUERY_STRING"); // ← SOURCE: attacker-controlled 4 // ... parse and extract param ... 5 return url_decode(val); // ← URL decode ≠ sanitization! 6} 7 8void search_products(void) { 9 char* term = get_cgi_param("q"); // ← tainted 10 if (!term) return; 11 12 // LLM added a length check — devs think this is "validation" 13 if (strlen(term) > 200) { free(term); return; } 14 15 SQLCHAR query[1024]; 16 snprintf((char*)query, sizeof(query), 17 "SELECT id,name,price FROM products WHERE name LIKE '%%%s%%'", 18 term); // ← taint injected into LIKE 19 SQLExecDirect(g_stmt, query, SQL_NTS); // ← SINK 💀 20 free(term); 21}
?q=' UNION SELECT username,password,'1' FROM admin_users --| What the LLM Did | Why It Seems Safe | Why It's Not |
|---|---|---|
url_decode() | Looks like input processing | Decodes %27 back to ' — expands attack surface |
strlen() check | Appears to limit input | ' UNION SELECT…-- is only 30 chars. Length ≠ safety |
Bounded snprintf | Prevents buffer overflow | Buffer safety ≠ SQL safety |
| Nice comments | Gives reviewer confidence | Comments don't execute. Data flow is what matters |
SAFE — Parameterized LIKE 1void search_products(void) { 2 char* term = get_cgi_param("q"); 3 if (!term) return; 4 char pattern[256]; 5 snprintf(pattern, sizeof(pattern), "%%%s%%", term); 6 const char* sql = "SELECT id,name,price FROM products WHERE name LIKE ?"; 7 SQLPrepare(g_stmt, (SQLCHAR*)sql, SQL_NTS); 8 SQLLEN len = SQL_NTS; 9 SQLBindParameter(g_stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, A SQL_CHAR, 256, 0, pattern, 0, &len); // ← taint neutralized B SQLExecute(g_stmt); C free(term); D}
sprintf + SQL snippets — including vulnerable ones. They cannot reason about taint propagation across functions. They produce code that looks correct but is exploitable.
SQL injection isn't a single vulnerability — it's an infinite class of data flow problems.
Every new function creates new taint paths. Safe yesterday → vulnerable after one commit.
"The wrapper is safe." "It was validated elsewhere." Only data flow analysis can verify.
AI copilots produce well-commented code with classic injection patterns. Can't reason about taint.
Taint traverses 10+ calls across 5+ files. Only automated inter-procedural analysis can find them.
Customize how BD-SECURITY-TDSQL detects taint sources, sanitizers, and data flow paths.
* for any)*When enabled, numerical data is never considered tainted — reduces false positives for numeric IDs.
BD-SECURITY-TDSQL maps to the following industry standards.
SQL injection has been known for 30 years — but every new function, every refactor, every LLM suggestion can create a fresh zero-day.
Automated inter-procedural taint analysis is the only reliable defense.