Безопасность Go приложений: топ-10 уязвимостей на практике
Go (Golang) — язык, который славится своей производительностью и встроенными механизмами безопасности. Однако, как и любой инструмент, он не защищает разработчика от ошибок полностью. Многие уязвимости возникают не из-за самого языка, а из-за неправильного использования его возможностей: работы с горутинами, каналами, указателями или сторонними библиотеками.
В этой статье мы разберём 10 самых распространённых уязвимостей в Go-приложениях. Каждый пункт содержит реальный пример кода, объяснение проблемы и практический совет по её устранению. Материал будет полезен как начинающим Go-разработчикам, так и опытным инженерам, желающим освежить знания.
1. SQL-инъекции через конкатенацию строк
Самая классическая ошибка — формирование SQL-запроса путём склейки строк с пользовательскими данными. Go предоставляет отличные драйверы (например, database/sql), но не защищает от глупых ошибок.
Уязвимый код:
func getUser(db *sql.DB, username string) (*User, error) {
query := "SELECT id, name FROM users WHERE username = '" + username + "'"
rows, err := db.Query(query)
// ...
}
Злоумышленник может передать admin' OR '1'='1 и получить всех пользователей.
Решение: Всегда используйте параметризованные запросы.
func getUserSafe(db *sql.DB, username string) (*User, error) {
query := "SELECT id, name FROM users WHERE username = $1"
row := db.QueryRow(query, username)
// ...
}
2. Race condition (гонка данных) в горутинах
Go — язык с мощной поддержкой конкурентности, но без синхронизации доступ к общим данным из разных горутин приводит к неопределённому поведению.
Уязвимый код:
var counter int
func increment() {
for i := 0; i < 1000; i++ {
counter++ // не атомарно
}
}
func main() {
go increment()
go increment()
time.Sleep(time.Second)
fmt.Println(counter) // результат непредсказуем
}
Решение: Используйте мьютексы (sync.Mutex) или атомарные операции (sync/atomic).
var mu sync.Mutex
var counter int
func incrementSafe() {
for i := 0; i < 1000; i++ {
mu.Lock()
counter++
mu.Unlock()
}
}
3. Утечка горутин (goroutine leak)
Если запущенная горутина блокируется навсегда (например, ожидает данные в канале, который никогда не закроется), она «утекает», потребляя память.
Уязвимый код:
func process(jobs <-chan int) {
for j := range jobs {
go func(job int) {
time.Sleep(time.Hour) // долгая работа
fmt.Println(job)
}(j)
}
}
Если канал jobs не закрыт, горутины будут висеть вечно.
Решение: Используйте контекст (context.Context) для отмены операций и всегда контролируйте время жизни горутин.
func processSafe(ctx context.Context, jobs <-chan int) {
for j := range jobs {
select {
case <-ctx.Done():
return
default:
go func(job int) {
select {
case <-time.After(time.Hour):
fmt.Println(job)
case <-ctx.Done():
return
}
}(j)
}
}
}
4. Неправильная обработка ошибок (panic/recover)
Использование panic() для обработки обычных ошибок — плохая практика. Если panic не перехватить, приложение упадёт. Но и злоупотребление recover() может скрыть критические проблемы.
Уязвимый код:
func riskyOperation(data string) {
if data == "" {
panic("data is empty")
}
// ...
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
riskyOperation("")
}
Здесь panic — это неправильный способ вернуть ошибку. Лучше вернуть её как значение.
Решение: Используйте error как тип возвращаемого значения.
func riskyOperationSafe(data string) error {
if data == "" {
return errors.New("data is empty")
}
return nil
}
5. Инъекция команд ОС (OS command injection)
Выполнение системных команд с подстановкой пользовательского ввода — прямой путь к компрометации сервера.
Уязвимый код:
func runCommand(userInput string) {
cmd := exec.Command("sh", "-c", "echo "+userInput)
out, _ := cmd.Output()
fmt.Println(string(out))
}
Передача ; rm -rf / может привести к катастрофе.
Решение: Избегайте sh -c с конкатенацией. Используйте exec.Command с фиксированными аргументами.
func runCommandSafe(userInput string) {
cmd := exec.Command("echo", userInput) // аргумент экранируется
out, _ := cmd.Output()
fmt.Println(string(out))
}
6. Небезопасная десериализация
Использование encoding/gob, encoding/json или xml без валидации может привести к атакам типа «отказ в обслуживании» (DoS) или выполнению произвольного кода.
Уязвимый код:
type Config struct {
Plugin string
}
func loadConfig(data []byte) (*Config, error) {
var cfg Config
err := json.Unmarshal(data, &cfg)
// ...
// далее может быть загрузка Plugin как пакета
}
Злоумышленник может подставить путь к вредоносному файлу.
Решение: Ограничьте размер входных данных, используйте белые списки для полей и не доверяйте структурам из сети.
7. Использование слабых криптографических алгоритмов
Go имеет мощную стандартную библиоте