Почему даже 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> превратится в <script>
tmpl.Execute(w, data)
}
Всё, что вы передаёте через {{ .Comment }}, будет экранировано. Если пользователь введёт <script>alert('xss')</script>, на выходе получится <script>alert('xss')</script> — браузер покажет это как текст, а не выполнит.
Особые случаи: экранирование в атрибутах и JavaScript
html/template умный: он понимает, что вы пишете в атрибуте, и экранирует соответственно. Например:
<a href="/user/{{ .UserID }}">Профиль</a>
Если .UserID содержит кавычку, шаблонизатор экранирует её в " и атрибут останется целым.
Но если вы вставляете данные прямо в блок <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),
}