Руководство по безопасности в Go: предотвращение SQL-инъекций и XSS-атак
Обеспечение безопасности веб-приложений — одна из приоритетных задач разработчика. Большинство уязвимостей возникает из-за смешивания пользовательских данных с исполняемым кодом. Когда необработанные данные, полученные от клиента, передаются интерпретатору, система воспринимает их как инструкции. Если этот интерпретатор — база данных, возникает уязвимость внедрения SQL-кода (SQL-инъекция). Если это веб-браузер пользователя, запускается сценарий межсайтового скриптинга (XSS).
В данном руководстве разбираются практические методы защиты веб-приложений на Go от этих двух векторов атак, с примерами безопасного написания кода, архитектурными правилами и настройками HTTP-заголовков.
Часть 1. Предотвращение SQL-инъекций в базах данных
Защита от SQL-инъекций происходит на уровне сервера, в кодовой базе приложения, взаимодействующей с СУБД (системой управления базами данных).
SQL-инъекция (SQL Injection) — уязвимость, при которой злоумышленник передает в текстовое поле специально сформированную строку. Если она склеивается напрямую с телом SQL-запроса, база данных выполняет непредусмотренные команды.
Опасный код: форматирование строки запроса
Распространенной ошибкой является использование стандартного форматирования строк для подстановки аргументов:
// Внимание: этот код уязвим к SQL-инъекциям!
query := fmt.Sprintf("SELECT id, email FROM users WHERE email = '%s'", email)
row := db.QueryRow(query)
Если пользователь передаст в качестве параметра email строку вида ' OR 1=1 --, итоговый запрос превратится в SELECT id, email FROM users WHERE email = '' OR 1=1 --'. Условие 1=1 всегда истинно, а символы -- закомментируют остаток запроса. СУБД проигнорирует проверку пароля и вернет всю таблицу пользователей.
Безопасный код: параметризованные запросы
Правильный подход заключается в строгом разделении текста SQL-запроса и данных. Для этого используются параметризованные запросы (подготовленные выражения):
// Безопасный вариант с передачей аргументов отдельно от запроса
row := db.QueryRowContext(ctx, "SELECT id, email FROM users WHERE email = $1", email)
Драйвер базы данных отправляет текст запроса и параметры отдельно. СУБД сначала компилирует структуру запроса (строит план выполнения), а затем подставляет параметры в качестве литералов. Значение параметра не может повлиять на синтаксическое дерево запроса.
Примечание: Синтаксис плейсхолдеров зависит от драйвера. В драйвере PostgreSQL pgx или lib/pq используется $1, $2, $3. В драйвере MySQL go-sql-driver/mysql применяется знак ?.
Что нельзя параметризовать: белые списки
Параметризовать можно только значения. СУБД не позволяет использовать плейсхолдеры для указания имен таблиц, колонок или направления сортировки (ORDER BY). Если нужно предоставить пользователю возможность сортировки по разным полям, используйте технику разрешенных значений (белых списков):
// Безопасная подстановка параметров через белый список
var allowedSort = map[string]string{
"created": "created_at",
"email": "email",
}
userSort := r.URL.Query().Get("sort")
column, ok := allowedSort[userSort]
if !ok {
column = "created_at"
}
query := fmt.Sprintf("SELECT id FROM users ORDER BY %s DESC LIMIT 50", column)
row := db.QueryRowContext(ctx, query)
Динамические списки параметров (оператор IN)
При использовании оператора IN, когда количество аргументов заранее неизвестно, приходится динамически формировать список плейсхолдеров в коде:
placeholders := make([]string, len(ids))
args := make([]any, len(ids))
for i, id := range ids {
placeholders[i] = fmt.Sprintf("$%d", i+1)
args[i] = id
}
query := "SELECT id FROM users WHERE id IN (" + strings.Join(placeholders, ",") + ")"
rows, err := db.QueryContext(ctx, query, args...)
Если вы используете PostgreSQL и драйвер pgx, задачу можно упростить с помощью конструкции = ANY:
query := "SELECT id FROM users WHERE id = ANY($1)"
rows, err := db.QueryContext(ctx, query, ids)
Популярные конструкторы запросов (например, squirrel) или ORM (например, GORM) автоматизируют подстановку аргументов под капотом. Однако помните, что методы для работы с сырыми фрагментами SQL (например, .Where()), передача непараметризованных данных в которые открывает уязвимость.
Дополнительные меры защиты базы данных:
- Принцип наименьших привилегий: Подключайте Go-приложение к базе данных под учетной записью пользователя с минимально необходимыми правами (
SELECT,INSERT,UPDATE). Запретите операции изменения структуры данных (ALTER,DROP,CREATE). - Логирование без конфиденциальных данных: Убедитесь, что логи СУБД или приложения не записывают персональные данные пользователей в открытом виде.
- Валидация длины: Ограничивайте максимальный размер входных текстовых полей на уровне веб-сервера.
Часть 2. Защита от межсайтового скриптинга (XSS) в веб-интерфейсах
Защита от межсайтового скриптинга реализуется на уровне веб-сервера Go в обработчиках HTTP-запросов (хендлерах), возвращающих HTML-код веб-браузеру клиента.
Межсайтовый скриптинг (XSS - Cross-Site Scripting) — уязвимость, позволяющая злоумышленнику внедрить вредоносный JavaScript-код в веб-страницу, которая отображается у других пользователей. Это может привести к краже файлов куки (cookies), токенов сессий и подмене интерфейса страницы.
Опасный код: прямой вывод в поток ответа
Если приложение отправляет данные пользователя клиенту напрямую без экранирования, браузер выполнит любой переданный скрипт:
// Внимание: этот код уязвим к XSS!
fmt.Fprintf(w, "<div>Привет, %s</div>", name)
Использование html/template
Стандартная библиотека Go предоставляет два пакета для работы с шаблонами: text/template и html/template. Для веб-приложений всегда нужно использовать html/template.
// Использование безопасного html/template
tmpl := template.Must(template.New("page").Parse(`<div>Привет, {{ .Name }}</div>`))
tmpl.Execute(w, data)
Пакет html/template осуществляет контекстное экранирование. Он анализирует HTML-документ и применяет разные правила экранирования в зависимости от того, куда попадает переменная:
- Внутри HTML-тегов: Символы
<,>,&заменяются на их HTML-сущности (<,>,&). - Внутри атрибутов тегов (например,
title="..."): Применяется экранирование кавычек. - Внутри ссылок (
href="..."): Проверяется протокол. Если в переменной содержится опасная ссылка вродеjavascript:alert(1), шаблонизатор заменит ее на безопасное значение#ZgotmplZ. - Внутри блоков
<script>: Данные экранируются по правилам синтаксиса JavaScript.
Санитаризация пользовательского HTML
Иногда приложению необходимо разрешить пользователю публиковать размеченный текст (например, статьи с тегами жирного шрифта). Пакет html/template позволяет отключить экранирование с помощью типов template.HTML, template.JS или template.URL.
!WARNING Никогда не приводите необработанные данные пользователя к типу
template.HTMLнапрямую: это полностью отключает защиту и делает приложение уязвимым к атакам.
Для безопасного вывода форматированного текста используйте специализированную библиотеку-санитайзер bluemonday. Она вырезает все опасные теги и атрибуты (такие как <script>, onload, onerror), оставляя только разрешенную разметку из белого списка:
import "github.com/microcosm-cc/bluemonday"
p := bluemonday.UGCPolicy()
cleanedHTML := p.Sanitize(userInput)
data.Bio = template.HTML(cleanedHTML)
Настройка заголовков безопасности и Cookie
Помимо экранирования, необходимо укрепить защиту веб-приложения на уровне протокола HTTP:
- Контроль типа контента (nosniff): При отдаче JSON-ответов из API всегда устанавливайте корректный заголовок
Content-Typeи запрещайте браузеру самостоятельно угадывать тип данных (mime-sniffing), чтобы исключить выполнение JSON как HTML-разметки:w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("X-Content-Type-Options", "nosniff") json.NewEncoder(w).Encode(resp) - Флаг HttpOnly для Cookie: При сохранении сессионных куки всегда устанавливайте флаг
HttpOnly. Это гарантирует, что JavaScript-код на клиенте не сможет прочитать значение куки через обращение кdocument.cookie. - Content-Security-Policy (CSP): Настройте заголовок
Content-Security-Policy, запрещающий выполнение инлайновых скриптов (unsafe-inline) и ограничивающий источники загрузки внешних ресурсов только доверенными доменами. Это станет мощным вторым рубежом обороны вашего веб-сервиса.

