Go против хакеров: защита от SQL-инъекций и XSS на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему даже Go-разработчики попадаются на удочку хакеров?

Go славится своей скоростью, простотой и встроенными инструментами безопасности. Но успокаивать себя фразой «я пишу на Go, значит, я в безопасности» — опасная иллюзия. SQL-инъекции и XSS-атаки — это не пережитки прошлого. Они до сих пор ежедневно валят сайты и сливают базы данных, и Go не является исключением.

Проблема в том, что Go даёт разработчику мощные инструменты, но не использует их за вас. Вы можете случайно склеить SQL-запрос через fmt.Sprintf или вывести пользовательский комментарий без экранирования — и злоумышленник получит доступ к вашей базе или украдёт куки посетителей.

В этой статье мы разберём два самых опасных типа атак — SQL-инъекции и Cross-Site Scripting (XSS) — и покажем, как их предотвратить с помощью нативного Go и популярных библиотек. Вы получите не теорию, а готовые шаблоны кода, которые можно вставить в свой проект уже сегодня.

SQL-инъекции в Go: как они возникают и как их убить на корню

Типичная ошибка: конкатенация строк

Представьте, что у вас есть endpoint для получения пользователя по ID. Самый наивный (и опасный) способ написать запрос выглядит так:

package main

import (
    "database/sql"
    "fmt"
    "log"
    _ "github.com/lib/pq"
)

func getUserByID(db *sql.DB, userID string) {
    // ОПАСНО! Конкатенация строк
    query := fmt.Sprintf("SELECT id, name, email FROM users WHERE id = '%s'", userID)
    rows, err := db.Query(query)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()
    // ... обработка
}

Если злоумышленник передаст в userID строку 1' OR '1'='1, запрос превратится в:

SELECT id, name, email FROM users WHERE id = '1' OR '1'='1'

Вуаля — он получит всех пользователей базы данных. А если он введёт 1'; DROP TABLE users; --, вы потеряете таблицу.

Решение: параметризованные запросы (prepared statements)

Go-драйвер для PostgreSQL, MySQL и SQLite поддерживает подготовленные выражения. Они отделяют код SQL от данных, и драйвер автоматически экранирует опасные символы.

func getUserByIDSafe(db *sql.DB, userID string) {
    // БЕЗОПАСНО: параметризованный запрос
    query := "SELECT id, name, email FROM users WHERE id = $1"
    rows, err := db.Query(query, userID)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()
    // ... обработка
}

Обратите внимание на $1 — это плейсхолдер для первого аргумента. В MySQL используется символ ?. Теперь даже если userID содержит 1' OR '1'='1, драйвер воспримет это как строковое значение, а не как часть SQL-кода.

Работа с пакетом sqlx

Библиотека github.com/jmoiron/sqlx делает код ещё чище. Она поддерживает именованные параметры и автоматическое сканирование в структуры:

import "github.com/jmoiron/sqlx"

type User struct {
    ID    int    `db:"id"`
    Name  string `db:"name"`
    Email string `db:"email"`
}

func getUserByIDSqlx(db *sqlx.DB, userID string) (*User, error) {
    user := &User{}
    // Именованный параметр :id
    err := db.Get(user, "SELECT * FROM users WHERE id = :id", sqlx.Named("id", userID))
    if err != nil {
        return nil, err
    }
    return user, nil
}

Важно: даже если вы используете ORM вроде GORM или Ent, убедитесь, что она настроена на использование подготовленных выражений. Некоторые старые версии ORM допускали «сырые» запросы.

XSS в Go: когда ваш сайт стреляет себе в ногу

Как происходит XSS-атака

XSS (Cross-Site Scripting) возникает, когда вы вставляете пользовательский контент на страницу без экранирования. Злоумышленник может отправить комментарий со скриптом:

<script>alert('Ваши куки украдены: ' + document.cookie)</script>

Если вы просто выведете этот текст в HTML, браузер выполнит скрипт. В реальных атаках скрипты воруют сессионные куки, перенаправляют на фишинговые сайты или устанавливают кейлоггеры.

Встроенное спасение: html/template

Go предоставляет пакет html/template (не путать с text/template!). Он автоматически экранирует вывод в зависимости от контекста: HTML, атрибуты, JavaScript, CSS.

package main

import (
    "html/template"
    "net/http"
)

func commentHandler(w http.ResponseWriter, r *http.Request) {
    // Пользовательский ввод (потенциально опасный)
    userComment := r.FormValue("comment")

    // Шаблон с экранированием
    tmpl := template.Must(template.New("comment").Parse(`
        <div class="comment">
            <p>{{ .Comment }}</p>
        </div>
    `))

    data := struct {
        Comment string
    }{
        Comment: userComment,
    }

    // При рендеринге <script> превратится в &lt;script&gt;
    tmpl.Execute(w, data)
}

Всё, что вы передаёте через {{ .Comment }}, будет экранировано. Если пользователь введёт <script>alert('xss')</script>, на выходе получится &lt;script&gt;alert('xss')&lt;/script&gt; — браузер покажет это как текст, а не выполнит.

Особые случаи: экранирование в атрибутах и JavaScript

html/template умный: он понимает, что вы пишете в атрибуте, и экранирует соответственно. Например:

<a href="/user/{{ .UserID }}">Профиль</a>

Если .UserID содержит кавычку, шаблонизатор экранирует её в &quot; и атрибут останется целым.

Но если вы вставляете данные прямо в блок <script>, нужно использовать специальный тип template.JS:

tmpl := template.Must(template.New("page").Parse(`
    <script>
        var userName = {{ .UserName }};
    </script>
`))

// Для контекста JavaScript используем template.JSStr
data := struct {
    UserName template.JSStr
}{
    UserName: template.JSStr(userInput),
}

Блоги

Рекомендаций по книгам