Go: SQL

Теория: Передача параметров и защита от SQL-инъекций

Плейсхолдеры и передача параметров

Когда программе нужно передать значения в SQL-запрос, вместо прямой вставки текста используются плейсхолдеры. Приложение отправляет в базу текст запроса отдельно, а значения — отдельным списком аргументов. Драйвер сам подставляет значения в обозначенные позиции и не смешивает их с SQL-кодом. Формат плейсхолдеров зависит от СУБД: PostgreSQL использует $1, $2, $3, а MySQL и SQLite — ?.

ctx := context.Background()

// PostgreSQL: значения идут отдельно, позиционно.
var id int
err := db.QueryRowContext(ctx,
	`INSERT INTO users(email, name) VALUES($1, $2) RETURNING id`,
	"alex@example.com", "Алексей",
).Scan(&id)
if err != nil {
	log.Fatal(err)
}

// MySQL / SQLite: тот же принцип, но с '?'.
_, err = db.ExecContext(ctx,
	`UPDATE products SET price = ? WHERE name = ?`,
	110000, "Ноутбук",
)
if err != nil {
	log.Fatal(err)
}

Конкатенирование SQL-строки

Когда значение попадает в SQL через конкатенацию строк, программа перестаёт различать, где заканчиваются данные и начинается код. Любой ввод превращается в часть SQL-команды, и база исполняет его как полноценный фрагмент запроса. При использовании плейсхолдеров такого смешивания не происходит: текст запроса остаётся неизменным, а значения передаются отдельно.

ctx := context.Background()

// Опасный вариант: значение становится частью SQL.
email := "' OR 1=1; DROP TABLE users; --"
query := "SELECT * FROM users WHERE email = '" + email + "'"

// База воспримет итоговую строку как полноценный SQL-код.
_, _ = db.QueryContext(ctx, query)

// Безопасный вариант: значение передаётся как параметр.
_, _ = db.QueryContext(ctx,
	`SELECT * FROM users WHERE email = $1`,
	email,
)
// В этом случае email остаётся обычной строкой, а не SQL-кодом.

Безопасные шаблоны при работе с SQL в Go чаще всего сводятся к повторяющемуся «каркасу» функции доступа к данным. Такой каркас использует текст запроса с плейсхолдерами, context.Context для управления временем выполнения, методы ExecContext() или QueryRowContext()/QueryContext() и явную проверку ошибки. Значения всегда передаются отдельными аргументами, а не через сборку строки. Один и тот же шаблон можно применять для чтения, вставки и обновления данных, меняется только запрос и список параметров.

type User struct {
	ID    int
	Email string
	Name  string
}

// Пример шаблона: функция получает контекст, *sql.DB и параметры.
func CreateUser(ctx context.Context, db *sql.DB, email, name string) (int, error) {
	var id int

	// SQL содержит только плейсхолдеры.
const query = `
		INSERT INTO users(email, name)
		VALUES($1, $2)
		RETURNING id
	`

	// Значения передаются отдельными аргументами.
	err := db.QueryRowContext(ctx, query, email, name).Scan(&id)
	if err != nil {
		return 0, err
	}

	return id, nil
}

func GetUserByEmail(ctx context.Context, db *sql.DB, email string) (*User, error) {
	const query = `
		SELECT id, email, name
		  FROM users
		 WHERE email = $1
	`

	var u User

	// Тот же шаблон чтения одной строки через QueryRowContext + Scan.
	err := db.QueryRowContext(ctx, query, email).
		Scan(&u.ID, &u.Email, &u.Name)

	if err != nil {
		// Проверяем «нет строки»
		if errors.Is(err, sql.ErrNoRows) {
			return nil, nil // пользователь не найден
		}
		// Любая другая ошибка
		return nil, fmt.Errorf("query user by email: %w", err)
	}

	return &u, nil

IN и несколько значений

Когда запрос использует IN, значения тоже нужно передавать отдельно от SQL. Программа не вставляет список чисел прямо в строку, потому что длина списка и его содержимое меняются по ходу работы. В PostgreSQL для таких случаев удобно использовать конструкцию = ANY($1) и передавать срез как один параметр. В MySQL и SQLite список плейсхолдеров формируется программно, но сами значения всё равно передаются отдельным набором параметров.

ctx := context.Background()

// PostgreSQL: передаём массив целиком через ANY($1).
ids := []int{3, 7, 9}

rows, err := db.QueryContext(ctx,
	`SELECT id, name FROM products WHERE id = ANY($1)`,
	pq.Array(ids), // массив идёт как один параметр
)
if err != nil {
	log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
	var id int
	var name string

	if err := rows.Scan(&id, &name); err != nil {
		log.Fatal(err)
	}
}

// MySQL/SQLite: количество ? зависит от длины списка.
list := []int{3, 7, 9}

// Генерируем строку вида "(?,?,?)".
placeholders := strings.TrimRight(strings.Repeat("?,", len(ids)), ",")

// Превращаем []int в []interface{} для передачи в ExecContext/QueryContext.
args := make([]interface{}, len(list))
for i, v := range list {
	args[i] = v
}

rows, err = db.QueryContext(ctx, query, args...)
if err != nil {
	log.Fatal(err)
}
defer rows.Close()

имена колонок и параметры сортировки

В отличие от значений, имена колонок, направления сортировки и другие элементы синтаксиса нельзя передавать как параметры. Плейсхолдеры работают только для данных, а не для структурных частей SQL. Чтобы выбрать безопасный вариант, программа использует заранее определённый список доступных колонок или вариантов сортировки. Пользовательский ввод сравнивается с этим списком, и в запрос подставляется только разрешённая строка.

ctx := context.Background()

// Белый список допустимых вариантов сортировки.
allowed := map[string]string{
	"price_asc":  "price ASC",
	"price_desc": "price DESC",
	"name_asc":   "name ASC",
	"name_desc":  "name DESC",
}

// Пользователь передал критерий сортировки.
userInput := "price_desc"

// Если ввода нет в списке — выбираем безопасный вариант по умолчанию.
order, ok := allowed[userInput]
if !ok {
	order = "id ASC"
}

// order — заранее известная строка, а не данные пользователя.
query := `SELECT id, name, price FROM products ORDER BY ` + order + ` LIMIT $1 OFFSET $2`

rows, err := db.QueryContext(ctx, query, 20, 0)
if err != nil {
	log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
	var id int
	var name string
	var price int
	if err := rows.Scan(&id, &name, &price); err != nil {
		log.Fatal(err)
	}
}

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845