flawopen.com/SQL Injection/Go

SQL Injection in Go

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 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.

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
// userID comes straight from the request
query := fmt.Sprintf(
  "SELECT * FROM users WHERE id = %s",
  userID,
)
rows, err := db.Query(query)
FIXED
// value passed as a query argument
rows, err := db.Query(
  "SELECT * FROM users WHERE id = $1",
  userID,
)

Why the fix works

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.

Go-specific gotchas

Placeholder syntax is driver-specific, not part of the language

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.

fmt.Sprintf is the trap precisely because it's idiomatic everywhere else

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.

ORMs like GORM still expose raw-SQL escape hatches

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.

Common misconceptions

"Go's strong typing protects me"

Type safety and query-construction safety are unrelated properties — a well-typed string can still contain an injected SQL fragment.

"I validated the input as alphanumeric, so concatenation is fine"

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.

How to check if you're affected

grep -rn "fmt.Sprintf(" --include="*.go" . | grep -i "select\|insert\|update\|delete" grep -rn "db.Query(fmt\|db.Exec(fmt" --include="*.go" .
gosec (rule G201/G202, SQL string formatting/concatenation) run in CI catches this pattern automatically.

Prevention checklist

FAQ

Does GORM protect me automatically?

Its standard chainable query builder does. Its Raw() escape hatch only stays safe if you pass parameters separately rather than formatting the string first.

Is this specific to any particular database driver?

No — the underlying mistake (building the query string yourself) is driver-agnostic. Only the placeholder syntax for the fix differs between drivers.

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