VNX-GO-003 – SQL Injection via fmt.Sprintf
Overview
This rule flags Go code where SQL queries are constructed using fmt.Sprintf and passed directly to database/sql methods (Query, QueryRow, Exec, QueryContext, ExecContext) or GORM’s Raw. When user-controlled input is embedded in a SQL string through format verbs or string concatenation, an attacker can alter the query’s logic, bypass authentication, read arbitrary data, modify or delete records, and in some database configurations execute operating system commands. This is CWE-89: Improper Neutralization of Special Elements used in an SQL Command.
Severity: Critical | CWE: CWE-89 – SQL Injection | OWASP ASVS: V5.3 – Output Encoding and Injection Prevention
Go idiom note: Parameterized queries ARE the idiomatic Go default for
database/sql. TheDB.Query,DB.QueryRow, andDB.Execmethods all accept variadic arguments that are passed to the driver as parameters — the safe path is the natural API. Usingfmt.Sprintfto build SQL is an active step away from how the standard library is designed to be used.
Why This Matters
SQL injection is consistently ranked in the OWASP Top 10 because its impact is catastrophic and exploitation is straightforward. An attacker who can inject into a login query can authenticate as any user, including administrators, without knowing their password. An injected UNION SELECT can dump entire tables — customer records, payment data, credentials — in a single request. Destructive payloads (DROP TABLE, mass DELETE) can cause irreversible data loss. In databases like PostgreSQL that support COPY TO/FROM or stored procedures with filesystem access, SQL injection can escalate to remote code execution on the database host. The fmt.Sprintf pattern in Go is especially dangerous because it looks innocuous and is easy to introduce during rapid development.
OWASP ASVS v4.0 requirement V5.3.4 requires that all database queries use parameterized queries or stored procedures to prevent SQL injection. Requirement V5.3.5 extends this to all dynamic database queries, and V5.3.6 mandates that ORMs use parameterized queries.
What Gets Flagged
The rule matches any .go line where a database/sql query method or GORM’s Raw is called with a fmt.Sprintf-formatted argument. It covers both the standard library and the common ORM pattern.
// FLAGGED: user input interpolated directly into SQL query
func getUser(db *sql.DB, r *http.Request) (*User, error) {
id := r.FormValue("user_id")
row := db.QueryRow(fmt.Sprintf("SELECT * FROM users WHERE id = '%s'", id))
// Attacker sends: user_id=' OR '1'='1
// Resulting query: SELECT * FROM users WHERE id = '' OR '1'='1'
// Returns every user in the table
var u User
return &u, row.Scan(&u.ID, &u.Name, &u.Email)
}
// FLAGGED: GORM Raw query with fmt.Sprintf
func searchProducts(db *gorm.DB, r *http.Request) {
name := r.FormValue("name")
var products []Product
db.Raw(fmt.Sprintf("SELECT * FROM products WHERE name = '%s'", name)).Scan(&products)
}
Remediation
- Use parameterized queries with placeholder arguments. The
database/sqlpackage supports$1,$2, … placeholders (PostgreSQL) or?placeholders (MySQL, SQLite). Pass user values as separate arguments — they are never interpolated into the SQL string.
// SAFE: parameterized query; the driver handles escaping
func getUser(db *sql.DB, r *http.Request) (*User, error) {
id := r.FormValue("user_id")
row := db.QueryRow("SELECT id, name, email FROM users WHERE id = $1", id)
var u User
err := row.Scan(&u.ID, &u.Name, &u.Email)
return &u, err
}
- Use parameterized queries with GORM. GORM’s
WhereandRawboth accept positional placeholders:
// SAFE: GORM parameterized query
func searchProducts(db *gorm.DB, r *http.Request) {
name := r.FormValue("name")
var products []Product
db.Where("name = ?", name).Find(&products)
// Or with Raw:
db.Raw("SELECT * FROM products WHERE name = ?", name).Scan(&products)
}
Validate and sanitize inputs before use. Parameterization is the primary defence, but also validate that input matches the expected format (e.g., integers should parse as integers, UUIDs should match UUID format) and reject unexpected values early.
Apply the principle of least privilege to the database user. The database account your application connects with should have only the permissions it needs:
SELECTon read-only queries,INSERT/UPDATEon write paths — neverDROP,CREATE, orALTERin production.Never use
fmt.Sprintfto build any part of a SQL query. This includes table names, column names, andORDER BYclauses. For dynamic identifiers use a strict allowlist to map user input to hard-coded SQL fragments.
References
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command
- OWASP Application Security Verification Standard v4.0 – V5.3 Output Encoding and Injection Prevention
- OWASP SQL Injection Prevention Cheat Sheet
- OWASP Go Security Cheat Sheet
- Go database/sql package documentation
- GORM Security documentation
- CAPEC-66: SQL Injection
- MITRE ATT&CK T1190 – Exploit Public-Facing Application