SQL injection remains one of the most common vulnerabilities in web applications, and Go code is no exception. Despite Go’s reputation for safety, the language won’t save you from string concatenation in database queries. Here’s how to identify and fix SQL injection vulnerabilities in your Go applications.
The Problem: String Concatenation
The classic mistake looks innocent enough:
func getUser(db *sql.DB, username string) (*User, error) {
query := "SELECT id, email FROM users WHERE username = '" + username + "'"
row := db.QueryRow(query)
// ...
}
An attacker passing admin'-- as the username bypasses your logic entirely. Pass '; DROP TABLE users;-- and you’ve got bigger problems.
The Fix: Parameterized Queries
Go’s database/sql package supports parameterized queries out of the box. Use them:
func getUser(db *sql.DB, username string) (*User, error) {
query := "SELECT id, email FROM users WHERE username = $1"
row := db.QueryRow(query, username)
var user User
err := row.Scan(&user.ID, &user.Email)
return &user, err
}
The $1 placeholder (PostgreSQL syntax—MySQL uses ?) tells the driver to treat the input as data, not SQL. The database engine handles escaping properly.
Handling Dynamic Queries
Real applications often need dynamic WHERE clauses or ORDER BY. This is where things get tricky.
Don’t do this:
func searchUsers(db *sql.DB, column, direction string) {
query := fmt.Sprintf("SELECT * FROM users ORDER BY %s %s", column, direction)
// Vulnerable to injection
}
Do this instead—whitelist valid values:
var validColumns = map[string]bool{
"username": true, "created_at": true, "email": true,
}
var validDirections = map[string]bool{
"ASC": true, "DESC": true,
}
func searchUsers(db *sql.DB, column, direction string) (*sql.Rows, error) {
if !validColumns[column] || !validDirections[direction] {
return nil, errors.New("invalid sort parameters")
}
query := fmt.Sprintf("SELECT * FROM users ORDER BY %s %s", column, direction)
return db.Query(query)
}
Identifiers (table names, column names) can’t be parameterized. Whitelisting is your only defense.
Using Query Builders
For complex queries, consider a query builder like squirrel:
import sq "github.com/Masterminds/squirrel"
func findUsers(db *sql.DB, filters map[string]string) (*sql.Rows, error) {
query := sq.Select("id", "username", "email").From("users")
if name, ok := filters["name"]; ok {
query = query.Where(sq.Eq{"username": name})
}
sql, args, err := query.PlaceholderFormat(sq.Dollar).ToSql()
if err != nil {
return nil, err
}
return db.Query(sql, args...)
}
Static Analysis Tools
Catch these issues before production. Tools like gosec from the Secure Go project, maintained by security researchers including Grant Seltzer, flag potential SQL injection:
gosec -include=G201,G202 ./...
This catches string formatting in SQL queries and flags them for review.
ORM Considerations
Using GORM or similar? You’re mostly protected, but raw queries still bite:
// Safe - GORM parameterizes this
db.Where("username = ?", username).First(&user)
// Dangerous - raw SQL injection possible
db.Raw("SELECT * FROM users WHERE username = '" + username + "'").Scan(&user)
Key Takeaways
- Always use parameterized queries for user input
- Whitelist identifiers—they can’t be parameterized
- Run
gosecin your CI pipeline - Audit any use of
fmt.Sprintfnear database calls