Description
SQL injection occurs when user input is concatenated or formatted directly into an executed SQL statement. For example, building "SELECT ... WHERE id = " + userInput lets input such as 1 OR 1=1 or 1; DROP TABLE users-- change query structure. Authentication bypass and data disclosure, modification or deletion depend on database permissions, the driver and its settings. Support for executing multiple statements also depends on the driver and configuration.
Potential impact
- Attackers may retrieve other users' personal data, account information or internal settings.
- Available database permissions may allow data modification, deletion or dropping tables and schemas.
- Injected conditions such as
OR 1=1may bypass authentication or authorization checks. - Expensive queries or destructive operations may disrupt the database and service.
Remediation
- Use driver-supported placeholders such as
?or$1indb.Query,db.QueryRowanddb.Exec, and supply values as separate arguments. - Use value-binding APIs in ORMs and query builders too; never concatenate user input into raw SQL.
- Validate numeric IDs with functions such as
strconv.Atoiand handle conversion failures. - Select SQL elements that cannot be bound, such as column names or sort directions, only from a code-defined allow-list.
- Do not give the database account unnecessary permissions such as
DROPorALTER. - Keep detailed database errors in protected internal logs and return generic errors to users.
Examples
These handler excerpts compare SQL parameter passing. They assume a driver that supports ?; use the placeholder syntax required by the actual driver. Authentication, authorization and response output handling are separate requirements.
Before
go
package main
import (
"database/sql"
"net/http"
)
// Before: a handler that concatenates input into SQL
func unsafeUserDetailHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Read id directly as a query-parameter string
userID := r.URL.Query().Get("id")
// Unsafe: build SQL by concatenating user input
// Example: /user?id=1 OR 1=1 --
query := "SELECT name, email FROM users WHERE id = " + userID
row := db.QueryRow(query)
var name, email string
if err := row.Scan(&name, &email); err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
w.Write([]byte(name + " / " + email))
}
}
After
go
package main
import (
"database/sql"
"net/http"
"strconv"
)
// After: a handler using a parameterized statement
func safeUserDetailHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userIDStr := r.URL.Query().Get("id")
// 1. Validate the type: reject non-numeric input
userID, err := strconv.Atoi(userIDStr)
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
// 2. Use a parameterized statement and placeholder
// - Bind values to placeholders such as `?` or `$1`
// so their string contents cannot change the query structure
const query = "SELECT name, email FROM users WHERE id = ?"
row := db.QueryRow(query, userID)
var name, email string
if err := row.Scan(&name, &email); err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Write([]byte(name + " / " + email))
}
}
Explanation:
- Before: The
idinput is concatenated into SQL and can alter its condition.OR 1=1may select more rows, but thisQueryRowcall reads only the first and discards the rest. Multiple-statement attacks depend on driver settings and database permissions. - After:
strconv.Atoichecks the numeric type, anduserIDis passed separately to fixed SQL so it is treated as data. Production error handling should distinguishsql.ErrNoRowsfrom a database failure.