Go

How do you prevent SQL injection with database/sql? Why can't placeholders be used for table names or ORDER BY columns?

Question 537MediumGo 1.22 to 1.25

Always pass user values as arguments to QueryContext, ExecContext or QueryRowContext, and never build SQL with fmt.Sprintf or +. The driver sends the query and the parameters separately (or escapes them correctly), so a value can never become SQL syntax. Placeholder syntax depends on the driver: ? for MySQL and SQLite, $1 for Postgres (pgx, lib/pq), @p1 for SQL Server.

Placeholders stand for values only. The server has to parse and plan the statement before binding, so identifiers such as table names, column names, ORDER BY targets and ASC/DESC are part of the statement's structure and cannot be bound. ORDER BY $1 either errors or sorts by a constant, which means it does nothing. The fix is an allowlist that maps user input to identifiers you control:

var sortCols = map[string]string{"name": "name", "created": "created_at"}

func listUsers(ctx context.Context, db *sql.DB, sortKey string, desc bool, limit int) (*sql.Rows, error) {
	col, ok := sortCols[sortKey]
	if !ok {
		col = "created_at"
	}
	dir := "ASC"
	if desc {
		dir = "DESC"
	}
	q := fmt.Sprintf("SELECT id, name FROM users ORDER BY %s %s LIMIT $1", col, dir)
	return db.QueryContext(ctx, q, limit) // values still parameterized
}

// value lookup: always parameterized
var name string
err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE email = $1", email).Scan(&name)

Gotchas: IN (...) lists need one generated placeholder per element (or = ANY($1) with an array in Postgres). LIKE input also needs % and _ escaped. Always defer rows.Close() and check rows.Err().

More on More Standard Library Essentials

All 16 More Standard Library Essentials questions