Безопасность Go приложений: топ-10 уязвимостей на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Безопасность 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 имеет мощную стандартную библиоте

Блоги

Рекомендаций по книгам