Безопасность Go приложений: топ-10 уязвимостей
Go (Golang) — компилируемый, статически типизированный язык, созданный в Google. Благодаря встроенной поддержке конкурентности, высокой производительности и простой модели памяти, он стал популярен для создания микросервисов, API-шлюзов, облачных приложений и инструментов DevOps. Однако, как и любой другой язык, Go не застрахован от ошибок безопасности. Разработчики часто полагаются на "безопасность по умолчанию", что приводит к появлению серьезных уязвимостей. В этой статье мы разберем топ-10 наиболее критичных проблем безопасности в Go-приложениях и покажем, как их избежать.
1. SQL-инъекции (SQL Injection)
Несмотря на то, что Go предоставляет мощные драйверы баз данных (database/sql), разработчики часто допускают классическую ошибку — конкатенацию строк при формировании запросов.
Уязвимый код:
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
func getUser(db *sql.DB, username string) {
// Никогда так не делайте!
query := fmt.Sprintf("SELECT * FROM users WHERE username = '%s'", username)
rows, _ := db.Query(query)
// ...
}
Решение: Используйте параметризованные запросы (prepared statements).
func getUserSafe(db *sql.DB, username string) {
query := "SELECT * FROM users WHERE username = $1"
rows, err := db.Query(query, username)
if err != nil {
// Обработка ошибки
}
// ...
}
Параметризация автоматически экранирует специальные символы, делая инъекцию невозможной.
2. Межсайтовый скриптинг (XSS)
Go часто используется для серверной части веб-приложений. Если вы возвращаете пользовательский ввод без экранирования, вы открываете путь для XSS-атак.
Уязвимый код (использование fmt.Fprintf):
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
fmt.Fprintf(w, "<h1>Привет, %s!</h1>", name) // XSS!
})
Решение: Используйте пакет html/template для автоматического экранирования.
import "html/template"
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.New("index").Parse("<h1>Привет, {{.}}!</h1>"))
name := r.URL.Query().Get("name")
tmpl.Execute(w, name) // Безопасно
})
Шаблонизатор html/template автоматически экранирует HTML, JavaScript и CSS контексты.
3. Состояния гонки (Race Conditions)
Go славится своей легковесной конкурентностью (горутины и каналы). Однако неправильное использование общих ресурсов (например, карт или счетчиков) без синхронизации приводит к data races.
Уязвимый код:
var counter int
func increment() {
for i := 0; i < 1000; i++ {
counter++ // Не атомарно!
}
}
func main() {
go increment()
go increment()
}
Решение: Используйте мьютексы (sync.Mutex) или атомарные операции (sync/atomic).
import "sync"
var (
counter int
mu sync.Mutex
)
func increment() {
for i := 0; i < 1000; i++ {
mu.Lock()
counter++
mu.Unlock()
}
}
Всегда запускайте тесты с флагом -race: go test -race.
4. Утечки памяти и горутин (Memory & Goroutine Leaks)
Одна из самых коварных уязвимостей в Go — "забытые" горутины. Если горутина блокируется навсегда (например, ожидает данные из канала, который никогда не закроется), это приводит к утечке памяти и отказу приложения.
Пример утечки:
func worker(ch chan int) {
for {
data := <-ch // Блокируется навсегда, если канал пуст и не закрыт
// Обработка data
}
}
func main() {
ch := make(chan int)
go worker(ch)
// ch никогда не закрывается и не получает данные
}
Решение: Используйте контексты (context.Context) для отмены горутин и всегда закрывайте каналы.
func worker(ctx context.Context, ch chan int) {
for {
select {
case <-ctx.Done():
return // Корректное завершение
case data, ok := <-ch:
if !ok {
return // Канал закрыт
}
// Обработка data
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch := make(chan int)
go worker(ctx, ch)
close(ch)
}
5. Неправильная обработка ошибок
В Go ошибки — это значения. Игнорирование ошибок (_) или их неправильная обработка может привести к раскрытию информации или нарушению логики безопасности.
Уязвимый код:
data, _ := ioutil.ReadFile("config.json") // Игнорируем ошибку
// Если файла нет, data будет nil, и приложение упадет или будет вести себя непредсказуемо
Решение: Всегда обрабатывайте ошибки. Используйте errors.Is и errors.As для проверки типов ошибок.
data, err := os.ReadFile("config.json")
if err != nil {
log.Fatalf("Не удалось прочитать файл: %v", err)
}
6. Небезопасное логирование (Log Injection)
Если вы логируете пользовательский ввод без фильтрации, злоумышленник может внедрить управляющие последовательности в лог-файлы (CRLF-инъекция) или подделать записи.
Уязвимый код:
log.Printf("