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.
SQL injection happens when user-controlled input gets inserted directly into a database query's text. In Go, the trap is almost always fmt.Sprintf — the language's idiomatic, go-to string-building tool — used to build a query instead of Go's own parameterized query support in database/sql.
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.// userID comes straight from the request
query := fmt.Sprintf(
"SELECT * FROM users WHERE id = %s",
userID,
)
rows, err := db.Query(query)
// value passed as a query argument
rows, err := db.Query(
"SELECT * FROM users WHERE id = $1",
userID,
)
database/sql's Query/Exec/QueryRow all accept values as variadic arguments and send them to the driver separately from the query text — the driver, not Go's string formatting, attaches the value to the query at the protocol level, where it can never be reinterpreted as SQL syntax.
The MySQL driver uses positional ?; lib/pq and pgx for PostgreSQL use numbered placeholders like $1. Go's standard library doesn't normalize this — check your specific driver's convention.
Go developers reach for Sprintf constantly for logging, error messages, and formatting — the exact same habit becomes dangerous the moment the resulting string is the SQL query itself, and nothing about the syntax signals the difference.
GORM's chainable query builder parameterizes automatically. Its Raw() method executes exactly what you pass it — safe only if you pass placeholders and args separately, not if you build the string with Sprintf first.
Type safety and query-construction safety are unrelated properties — a well-typed string can still contain an injected SQL fragment.
Validation logic is a separate code path from query construction and can drift out of sync over time (a refactor, a new caller) — parameterization removes the dependency on that validation being perfect forever.
grep -rn "fmt.Sprintf(" --include="*.go" . | grep -i "select\|insert\|update\|delete"
grep -rn "db.Query(fmt\|db.Exec(fmt" --include="*.go" .
Its standard chainable query builder does. Its Raw() escape hatch only stays safe if you pass parameters separately rather than formatting the string first.
No — the underlying mistake (building the query string yourself) is driver-agnostic. Only the placeholder syntax for the fix differs between drivers.