TGViewer
Библиотека Go-разработчика | Golang Библиотека Go-разработчика | Golang @goproglib · 24.1K subscribers
Post #7408 2.88K
🛡 Защита от SQL инъекций и XSS в Go

Оба типа атак растут из одной ошибки. Мы склеиваем строку из данных пользователя и отдаём её движку, который умеет исполнять код. База данных видит SQL, браузер видит HTML или JavaScript. Данные внезапно становятся командами.

Разберём, как этого не допускать на практике, и что спросят на собеседовании.

💉 SQL инъекции

Опасный код выглядит так:
query := fmt.Sprintf("SELECT id, email FROM users WHERE email = '%s'", email)
row := db.QueryRow(query)


Если в email придёт ' OR 1=1 --, условие развалится и вернётся вся таблица. Строка форматируется до того, как попадёт в базу, поэтому никакой защиты тут нет.

Правильный вариант передаёт значения отдельно от текста запроса:
row := db.QueryRowContext(ctx,
"SELECT id, email FROM users WHERE email = $1", email)


Драйвер отправляет запрос и параметры разными полями протокола. Значение уже не может изменить структуру запроса, потому что база разбирает синтаксис до подстановки данных. Синтаксис плейсхолдера зависит от драйвера, в pgx и lib/pq это $1, в go-sql-driver/mysql это ?.

Что нельзя параметризовать

Плейсхолдеры работают только для значений. Имя таблицы, имя колонки, направление сортировки так подставить не получится. Здесь помогает белый список:
var allowedSort = map[string]string{
"created": "created_at",
"email": "email",
}

column, ok := allowedSort[r.URL.Query().Get("sort")]
if !ok {
column = "created_at"
}

query := fmt.Sprintf("SELECT id FROM users ORDER BY %s DESC LIMIT 50", column)


Форматирование строки тут безопасно, потому что в column попадает только заранее известное значение из карты, а не пользовательский ввод.

Отдельно стоит помнить про 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...)


В pgx проще передать массив через = ANY($1).
Билдеры вроде squirrel и ORM вроде gorm подставляют аргументы сами. Но у обоих есть методы для сырых кусков SQL, и туда пользовательский ввод класть нельзя. gorm спасает не сам по себе, а только пока мы не пишем Where(fmt.Sprintf(...)).

Дополнительные меры, которые уменьшают ущерб при ошибке. Отдельный пользователь базы с минимальными правами, без DROP и без доступа к чужим схемам. Логи запросов без подстановленных значений. Ограничение на размер входных полей.

🔪 XSS

Та же логика, только теперь опасный движок это браузер. Если мы вставим пользовательский текст прямо в HTML, тег <script> из комментария выполнится у всех, кто открыл страницу.

Плохо:
fmt.Fprintf(w, "<div>Привет, %s</div>", name)


Хорошо:
tmpl := template.Must(template.New("page").Parse(
`<div>Привет, {{ .Name }}</div>`))
tmpl.Execute(w, data)


Ключевое отличие в пакете. html/template экранирует данные с учётом контекста, text/template не экранирует ничего. Импорт text/template в веб хендлере это готовая дыра, и на собеседовании про это спрашивают почти всегда.

Контекстность означает, что шаблонизатор смотрит, куда именно попадает значение. Внутри тега это HTML экранирование, внутри атрибута другое, внутри href проверка схемы, внутри блока <script> экранирование по правилам JavaScript.

Один и тот же {{ .Value }} даст разный вывод:
tmpl := template.Must(template.New("x").Parse(`
<a href="{{ .URL }}" title="{{ .Title }}">{{ .Title }}</a>
<script>var user = {{ .Name }};</script>
`))


Строка javascript:alert(1) в href превратится в #ZgotmplZ, то есть ссылка просто не сработает.

➡️ Где защита отключается

Типы template.HTML, template.JS, template.URL говорят шаблонизатору доверять содержимому и не трогать его. Это законная возможность, но применять её к данным пользователя нельзя:
// так делать нельзя
data.Bio = template.HTML(userInput)


Если пользователю действительно нужен HTML, например разметка в комментариях, содержимое надо чистить санитайзером:
import "github.com/microcosm-cc/bluemonday"

p := bluemonday.UGCPolicy()
safe := template.HTML(p.Sanitize(userInput))


bluemonday
работает по белому списку тегов и атрибутов, всё остальное вырезает.

➡️ API и заголовки

Для JSON ответов достаточно encoding/json, он экранирует <, > и & в escape последовательности. Но заголовок ставить обязательно, иначе браузер может угадать тип и отрендерить ответ как HTML.
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
json.NewEncoder(w).Encode(resp)


Хорошая практика добавить Content-Security-Policy, который запрещает инлайновые скрипты. Он не заменяет экранирование, но снижает шанс, что случайная дыра превратится в кражу сессий. Куки сессии стоит помечать HttpOnly, тогда украденный скрипт до них не доберётся.

📍 Навигация: Вакансии • Задачи • Собесы

🐸 Библиотека Go-разработчика

#GoDeep
  • ❤ 6
  • 🥰 3
  • 👍 1
  • 😢 1
More from @goproglib
  1. Sep 25, 2026🤡🤡 📍 Навигация: Вакансии • Задачи • Собесы 🐸 Библиотека Go-разработчика #GoGiggle
  2. Sep 25, 2026💡 Код работает. А data race уже есть В Go можно записать значение в одной горутине, прочи…
  3. Sep 23, 2026💥 TCP/IP: что происходит с данными в сети Когда Go-приложение отправляет данные по сети,…
  4. Sep 22, 2026🔴 Встроенные функции len, make, panic — встроенные функции Go, которые можно использовать…
  5. Sep 21, 2026🤩 Go с нуля: серия базовых шпаргалок Собрали путь от Hello, World! до goroutines, channel…
  6. Sep 20, 2026🤔 Вопрос с собеседования по Go Что выведет программа? ❤️ — [1 2 3 4 5] / [1 2 10] 🔥 — [1…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →