SQL injection from non-literal string concatenation in database/sql query

Critical Risk SQL Injection
gogolangsql-injectiondatabase-sqldatabase

What it is

SQL injection vulnerability in Go applications using the standard database/sql package where variables are concatenated into SQL strings without parameterization, allowing attackers to manipulate database queries.

package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
)

type User struct {
    ID       int
    Username string
    Email    string
}

func getUserByID(db *sql.DB, userID string) (*User, error) {
    // VULNERABLE: Direct string concatenation
    query := "SELECT id, username, email FROM users WHERE id = " + userID

    row := db.QueryRow(query)
    var user User
    err := row.Scan(&user.ID, &user.Username, &user.Email)
    return &user, err
}

func searchUsers(db *sql.DB, name, department string) ([]User, error) {
    // VULNERABLE: fmt.Sprintf with user input
    query := fmt.Sprintf(
        "SELECT id, username, email FROM users WHERE name LIKE '%%%s%%' AND department = '%s'",
        name, department
    )

    rows, err := db.Query(query)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var user User
        rows.Scan(&user.ID, &user.Username, &user.Email)
        users = append(users, user)
    }
    return users, nil
}

// Attack examples:
// userID: "1 OR 1=1 --"
// name: "'; DROP TABLE users; --"
package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
)

type User struct {
    ID       int
    Username string
    Email    string
}

func getUserByID(db *sql.DB, userID string) (*User, error) {
    // SECURE: Use ? placeholder instead of concatenation
    query := "SELECT id, username, email FROM users WHERE id = ?"

    row := db.QueryRow(query, userID)  // Pass userID as parameter
    var user User
    err := row.Scan(&user.ID, &user.Username, &user.Email)
    return &user, err
}

func searchUsers(db *sql.DB, name, department string) ([]User, error) {
    // SECURE: Use ? placeholders for all values
    query := "SELECT id, username, email FROM users WHERE name LIKE ? AND department = ?"

    rows, err := db.Query(query, "%"+name+"%", department)  // Parameters
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var user User
        rows.Scan(&user.ID, &user.Username, &user.Email)
        users = append(users, user)
    }
    return users, nil
}

// SECURE: Prepared statement for repeated operations
func getUsersByStatus(db *sql.DB, status string) ([]User, error) {
    stmt, err := db.Prepare("SELECT id, username, email FROM users WHERE status = ?")
    if err != nil {
        return nil, err
    }
    defer stmt.Close()

    rows, err := stmt.Query(status)  // Pass status as parameter
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var user User
        rows.Scan(&user.ID, &user.Username, &user.Email)
        users = append(users, user)
    }
    return users, nil
}

💡 Why This Fix Works

The vulnerable code uses string concatenation and fmt.Sprintf to build SQL queries, allowing injection attacks. The fixed version uses parameterized queries with ? placeholders, implements input validation, and demonstrates proper use of context and prepared statements.

Why it happens

A variable is concatenated into the SQL string and executed via database/sql without parameterization.

Root causes

Variable Concatenation in SQL Strings

A variable is concatenated into the SQL string and executed via database/sql without parameterization.

Preview example – GO
// VULNERABLE: Variable concatenation
func getUserByID(db *sql.DB, userID string) (*User, error) {
    query := "SELECT * FROM users WHERE id = " + userID
    rows, err := db.Query(query)
    // Allows injection through userID parameter
    return processUser(rows, err)
}

fmt.Sprintf in SQL Construction

Using fmt.Sprintf to build SQL queries with user input directly embeds untrusted data into the query structure.

Preview example – GO
// VULNERABLE: fmt.Sprintf usage
func searchUsers(db *sql.DB, name, department string) ([]User, error) {
    query := fmt.Sprintf(
        "SELECT * FROM users WHERE name = '%s' AND department = '%s'",
        name, department
    )
    rows, err := db.Query(query)
    return processUsers(rows, err)
}

Fixes

1

Use Parameterized Queries with Placeholders

Use parameterized queries. Replace concatenation and fmt.Sprintf with driver placeholders (? or $1) and arguments, e.g., db.QueryContext(ctx, 'SELECT ... WHERE id = ?', id).

View implementation – GO
// SECURE: Parameterized query
func getUserByID(db *sql.DB, userID string) (*User, error) {
    query := "SELECT * FROM users WHERE id = ?"
    rows, err := db.Query(query, userID)
    return processUser(rows, err)
}
2

Use Prepared Statements for Reuse

For reuse, prepare with db.PrepareContext and pass parameters to Exec/Query. This is especially efficient for repeated operations.

View implementation – GO
// SECURE: Prepared statement
func getUserByIDPrepared(db *sql.DB, userID string) (*User, error) {
    stmt, err := db.Prepare("SELECT * FROM users WHERE id = ?")
    if err != nil {
        return nil, err
    }
    defer stmt.Close()

    rows, err := stmt.Query(userID)
    return processUser(rows, err)
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies sql injection from non-literal string concatenation in database/sql query and many other security issues in your codebase.