Safeguard
Application Security•

How does SQL injection happen in Go, and how do you prevent it?

In Go, SQL injection comes from building queries with fmt.Sprintf or string joins. Pass values as arguments to database/sql so they stay data.

Safeguard Security Team
4 min read

In Go, SQL injection happens when user input is spliced into a query string, usually with fmt.Sprintf or string concatenation. The fix is to put placeholders in the SQL and pass values as separate arguments to database/sql, so the database treats them as data and never as part of the command.

What does vulnerable Go code look like?

The Go documentation on avoiding SQL injection risk shows the pattern to avoid. Assembling the statement with fmt.Sprintf and a %s verb builds the entire SQL string, with the parameter value inside it, before it is sent to the database.

// Unsafe: the value becomes part of the SQL text.
q := fmt.Sprintf("SELECT id, email FROM users WHERE name = '%s'", name)
rows, err := db.Query(q)

If name is an ordinary name, this works. If it contains a quote and extra SQL, the database cannot tell where your intended query ends. Go's documentation labels this approach insecure.

What does safe Go code look like?

Provide values as arguments, and use a placeholder in the statement.

rows, err := db.Query("SELECT id, email FROM users WHERE name = ?", name)

The Go documentation explains that the sql package turns the statement into a prepared statement and sends the parameter separately. The structure of the query is fixed before any user value arrives, so the value cannot change it.

Placeholder syntax depends on the database and driver. The Go docs note that the pq driver for Postgres uses a form such as $1 instead of ?. Check your driver's documentation.

Why do prepared statements work?

OWASP's SQL Injection Prevention Cheat Sheet lists the primary defenses in order: prepared statements with parameterized queries, properly constructed stored procedures, and allow-list input validation. It says escaping all user input is strongly discouraged. The reasoning is the same as the NCSC's contrast with prompt injection: parameterized queries enforce a separation between instructions and data, so whatever the input contains, the engine cannot interpret it as an instruction.

What cannot be parameterized?

Placeholders stand in for values. They cannot stand in for table names, column names or sort direction. A request like ?sort=price that ends up in an ORDER BY clause is a common trap. For these, use an allow list in code. Keep a small map from allowed request values to column names, look the value up, and fall back to a default or an error if it is missing.

A constructed example

A handler lists orders for a customer and lets the caller sort by "date" or "total". The first version formats both the customer id and the sort column into the SQL. A reviewer finds both. The fix passes the customer id as an argument and replaces the sort value with a lookup from "date" and "total" to column names the code owns. A unit test sends a sort value containing a quote and expects a 400 response, not a database error.

Static analysis can find the first pattern at scale. Safeguard's code scan (SAST) can run on every change, and the demo below shows it on a sample project.

See the full walkthrough at /demos/sast-code-scan.

What about ORMs and query builders?

Libraries that build queries for you generally use placeholders under the hood, which makes the safe path the default. They are not magic. Most also offer a raw SQL escape hatch, and that is where string formatting sneaks back in. Treat any call that accepts a raw SQL string as a place to review. The same goes for dynamic fragments such as a user-supplied filter expression. Check each library's documentation for how it handles raw fragments, and apply the same rule: values go in as arguments, identifiers come from an allow list.

How to check or apply this

  1. Search your Go code for fmt.Sprintf, string concatenation and strings.Builder near db.Query, db.Exec and db.QueryRow.
  2. Replace them with placeholders and arguments.
  3. Allow list identifiers such as sort columns and table names.
  4. Give the database account only the permissions the application needs, so a flaw does less damage.
  5. Add a test that sends quote characters and expects safe handling.
  6. Run static analysis in continuous integration to catch regressions.

Quick check

Is escaping quotes enough?

No. OWASP strongly discourages escaping as a primary defense. Use parameterized queries.

Can I parameterize a table name?

No. Placeholders are for values. Use an allow list for identifiers.

Sources

Last reviewed: 18 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.