article

Fixing SQL Injection in Go

3 min read

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