Skip to content
CWE-89A03:2021 – Injection

Prevention — Go

database/sql placeholders, why fmt.Sprintf into a query is the whole bug, and GORM's Raw vs interpolation.

The Rule

Pass values as variadic arguments to database/sql methods. The placeholder character is driver-specific — ? for MySQL and SQLite, $1 for PostgreSQL, @p1 or :name for MSSQL — but the principle is identical: the query string is constant, the values travel separately.

Go's trap is fmt.Sprintf. It is the idiomatic way to build strings in Go, which makes it the idiomatic way to write a SQL injection. Any Sprintf, +, or strings.Builder producing a query with user data in it is the vulnerability.

database/sql

Gousers.goVulnerable
// fmt.Sprintf builds the final string before the driver sees it.
q := fmt.Sprintf(
    "SELECT * FROM users WHERE name = '%s'", name)
rows, err := db.Query(q)

// Concatenation, identical problem
rows, err := db.Query(
    "SELECT * FROM users WHERE id = " + id)

GORM

GoVulnerable
// Sprintf into Raw or Where — injectable.
db.Raw(fmt.Sprintf(
    "SELECT * FROM users WHERE name = '%s'", name)).Scan(&users)

db.Where(fmt.Sprintf("name = '%s'", name)).Find(&users)

// Order with raw input (identifier context)
db.Order(sortParam).Find(&products)

sqlc and Compile-Time Safety

Go has a strong option unavailable in most languages: sqlc generates type-safe Go from SQL you write in .sql files, with parameters as named arguments. Because the queries are fixed at build time and parameters are always bound, there is no string-building surface for injection to exist in.

-- name: GetUserByName :one
SELECT * FROM users WHERE name = $1;

generates a GetUserByName(ctx, name) function that is parameterised by construction. Where the query set is known ahead of time, this eliminates the class rather than guarding against it.

pgx (used directly rather than through database/sql) similarly enforces parameterisation for its Query/Exec methods and is a good default for PostgreSQL.

Checklist

  • Values as variadic args to Query/QueryRow/Exec. Never fmt.Sprintf, +, or strings.Builder producing SQL with user data.
  • Match the driver's placeholder style (?, $1, @p1).
  • GORM: ? args in Raw/Where; struct/map conditions; allowlist for Order.
  • Consider sqlc or pgx to make parameterisation structural.
  • Identifiers: allowlist.
  • go vet does not catch this; add a linter such as golangci-lint with an SQL-injection analyzer, or Semgrep, to CI.
  • Least-privilege database role — see Defense in Depth.