3. OWASP Top 10 для backend разработчика

Введение: OWASP Top 10 как компас, а не чек-лист

«Простота — непременное условие надежности».
— Эдсгер Дейкстра

Проект OWASP Top 10 (Open Web Application Security Project) в глазах многих начинающих разработчиков выглядит как унылый бюрократический чек-лист, составленный аудиторами ради заполнения отчетов о соответствии. Однако опытный инженер видит в нем нечто совсем иное: это квинтэссенция боли, инцидентов и реальных взломов сотен тысяч продакшен-систем по всему миру.

Версия OWASP Top 10 2021 года сместила фокус с чисто технических уязвимостей на архитектурные и процессные ошибки (Insecure Design) и сбои в конфигурации окружения (Security Misconfiguration).

Для бэкенд-разработчика на Go это означает, что защита от большинства пунктов закладывается на уровне дизайна системы, работы с рантаймом и конфигурации сетевых примитивов. Go — язык со статической типизацией и автоматическим управлением памятью, который надежно защищает нас от классического переполнения стекового буфера в стиле C/C++. Но это порождает опасную иллюзию неуязвимости. Компилятор Go бессилен, если программист забыл проверить права владения ресурсом в хэндлере, открыл системный профайлер pprof на внешнем порту, допустил атаку по времени при валидации токена или позволил неконтролируемому вводу исчерпать память кучи и уронить процесс в OOM.

flowchart TD
    A["OWASP Top 10 2021"] --> B["1 - Broken Access Control"]
    A --> C["2 - Cryptographic Failures"]
    A --> D["3 - Injection"]
    A --> E["4 - Insecure Design"]
    A --> F["5 - Security Misconfiguration"]
    A --> G["6 - Vulnerable Components"]
    A --> H["7 - Auth Failures"]
    A --> I["8 - Data Integrity Failures"]
    A --> J["9 - Logging Failures"]
    A --> K["10 - SSRF"]

    style B fill:#7f1d1d,stroke:#f87171,color:#f8fafc
    style D fill:#7f1d1d,stroke:#f87171,color:#f8fafc
    style K fill:#7f1d1d,stroke:#f87171,color:#f8fafc
  

В этой статье мы подробно препарируем каждую из десяти категорий через призму реализации на Go, обращая внимание на поведение рантайма, работу с памятью, системные вызовы и типичные ловушки, которые всплывают на хардовых собеседованиях.


1. Broken Access Control

Класс уязвимостей, когда приложение не проверяет права доступа к ресурсам. В Go это часто проявляется из-за небрежной работы с context или отсутствия централизованного слоя авторизации.

Решение в коде:

Go
func (h *Handler) GetDocument(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    userID := auth.UserIDFromContext(ctx)
    if userID == 0 {
        http.Error(w, "unauthorized", http.StatusUnauthorized)
        return
    }

    docID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
    if err != nil || docID <= 0 {
        http.Error(w, "bad request", http.StatusBadRequest)
        return
    }

    // 🔒 Явная проверка владения до доступа к БД
    if err := h.authz.CheckOwnership(ctx, userID, "document", docID); err != nil {
        http.Error(w, "forbidden", http.StatusForbidden)
        return
    }

    doc, err := h.repo.GetByID(ctx, docID)
    if err != nil {
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }

    json.NewEncoder(w).Encode(doc)
}

2. Cryptographic Failures

Ранее известная как Sensitive Data Exposure. Ошибки здесь кроются не только в выборе алгоритма, но и в способе хранения ключей, ротации и обработке чувствительных данных в памяти.

Mechanical Sympathy: В Go строки иммутабельны. Если вы загрузите пароль в string, он останется в куче до следующей сборки мусора, а GC не гарантирует немедленное освобождение. Атакующий, сделавший дамп памяти процесса, может восстановить строку.

Безопасная работа с чувствительными данными:

Go
// ✅ Используйте []byte и явно затирайте память после использования
func processSecret(secret []byte) error {
    defer func() {
        for i := range secret {
            secret[i] = 0 // 🔒 Перезапись в куче перед выходом
        }
    }()
    // Работа с секретом...
    return nil
}

Для хеширования паролей используйте golang.org/x/crypto/argon2 или bcrypt. Подробнее в 1. Пароли и хранение хешей и 2. bcrypt, argon2 и выбор алгоритма.


3. Injection

SQL, OS Command, LDAP, XML инъекции. В Go риск снижается благодаря строгой типизации и database/sql с подготовленными выражениями, но не исчезает полностью.

Пример безопасного запроса:

Go
// ✅ Параметризованный запрос. Драйвер передаёт данные отдельно от текста запроса.
// На уровне ОС это часто приводит к оптимизации плана выполнения и кэшированию.
query := "SELECT id, role FROM users WHERE email = $1 AND status = $2"
row := db.QueryRowContext(ctx, query, email, "active")

Динамические команды ОС требуют жёсткого контроля:

Go
// ❌ Опасно: пользовательский ввод попадает в оболочку
// exec.Command("sh", "-c", fmt.Sprintf("ping %s", userInput))

// ✅ Безопасно: аргументы передаются как отдельные элементы массива, без интерпретации оболочкой
cmd := exec.Command("ping", "-c", "1", sanitizedHost)

4. Insecure Design

Новая категория в OWASP 2021. Означает, что бизнес-логика или архитектура изначально содержат уязвимости, которые невозможно закрыть патчем. Например, отсутствие лимитов на количество попыток восстановления пароля или передача внутренних ID в публичный API без маскировки.

В Go это часто проявляется при проектировании proto-контрактов или структур запросов:

Go
// ❌ Уязвимый дизайн: передача внутренних флагов в публичный DTO
type PublicUserResponse struct {
    ID       int64  `json:"id"`
    Name     string `json:"name"`
    IsAdmin  bool   `json:"is_admin"` // 🔴 Утечка прав доступа
    RoleCode string `json:"role_code"` // 🔴 Внутренний контракт
}

// ✅ Безопасно: явное разделение слоёв
type PublicUserResponse struct {
    ID   int64  `json:"id"`
    Name string `json:"name"`
}

5. Security Misconfiguration

Наиболее частый вектор атак на высоконагруженные Go-сервисы. Сюда входят: открытые pprof/expvar эндпоинты, дефолтные CORS-настройки, отсутствие таймаутов на http.Client или http.Server, слабые TLS-конфиги.

Механика и влияние на рантайм:
Если не настроить http.Server.ReadTimeout, net/http будет бесконечно читать тело запроса. Атакующий может открыть тысячи соединений и отправить заголовок, но не тело. Каждый такой коннект создаёт горутину (g), которая блокируется в syscall read. Планировщик runtime создаст новые треды (m / M), что приведёт к исчерпанию файловых дескрипторов (ulimit -n) и Out-of-Memory (атака Slowloris).

Go
srv := &http.Server{
    Addr:           ":8080",
    ReadTimeout:    15 * time.Second,  // 🔒 Защита от Slowloris
    WriteTimeout:   15 * time.Second,  // 🔒 Защита от медленных клиентов
    IdleTimeout:    60 * time.Second,  // 🔒 Очистка keep-alive соединений
    MaxHeaderBytes: 1 << 20,           // 🔒 Ограничение размера заголовков (1 МБ)
}

Подробнее о TLS в 3. TLS и HTTPS под капотом.


6. Vulnerable and Outdated Components

Go статически линкует зависимости, поэтому уязвимость в библиотеке становится частью вашего бинарника. В отличие от PHP или Java, где зависимости загружаются динамически или изолируются контейнером, в Go вы обязаны проверять go.mod, go.sum и прокси-кэш.

Инструменты: govulncheck, osv-scanner, dependabot. Не игнорируйте SECURITY.md в репозиториях зависимостей.


7. Identification and Authentication Failures

Ошибки в реализации логина, сессий, JWT, восстановления паролей. В Go специфика кроется в работе с context и состоянием.

Ловушка: Сравнение токенов через == подвержено timing-атаке. Сравнение строк в Go идёт посимвольно и останавливается при первом несовпадении. Атакующий может измерить время ответа и подобрать токен символ за символом.

Go
// ❌ Уязвимо к timing-атаке
if reqToken != expectedToken { 
    return ErrUnauthorized 
}

// ✅ Безопасно: постоянное время сравнения (CPU-уровень)
if subtle.ConstantTimeCompare([]byte(reqToken), []byte(expectedToken)) != 1 {
    // Утечка информации через время выполнения предотвращена
    return ErrUnauthorized
}

Подробнее про токены: 3. JWT. Устройство и подводные камни и 4. Access и refresh токены.


8. Software and Data Integrity Failures

Уязвимости, связанные с обновлением ПО, CI/CD пайплайнами и десериализацией непроверенных данных. В Go это проявляется при использовании encoding/json или gob с пользовательским вводом без ограничений.

JSON-бомбы и давление на GC:

Go
// ❌ Опасно: декодирование без ограничения глубины и размера
var payload HugeStruct
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { ... }
// Атакующий отправляет вложенный объект 10^5 уровней. Рантайм рекурсивно аллоцирует
// память, вызывая паузы GC и потенциальный OOM.
Go
// ✅ Безопасно: ограничение глубины и размера
dec := json.NewDecoder(io.LimitReader(r.Body, 1<<20)) // 1 МБ максимум
dec.DisallowUnknownFields() // 🔒 Защита от overposting
if err := dec.Decode(&payload); err != nil { ... }

9. Security Logging and Monitoring Failures

Отсутствие логов или, наоборот, логирование чувствительных данных. В Go переход на log/slog (с 1.21) решает многие проблемы, но требует дисциплины.

Go
// ❌ Опасно: структурированный лог всё равно содержит сырой JWT
logger.Info("request processed", 
    "token", r.Header.Get("Authorization"), 
    "ip", r.RemoteAddr)

// ✅ Безопасно: маскирование и фильтрация
logger.Info("request processed",
    "token_preview", maskToken(r.Header.Get("Authorization")),
    "user_id", userID,
    "ip", r.RemoteAddr)

Подробнее в 3. Логирование и аудит.


10. Server-Side Request Forgery (SSRF)

Атакующий заставляет ваш сервер делать запрос к внутренним ресурсам (например, http://169.254.169.254/latest/meta-data/ в AWS). В Go это часто происходит при реализации вебхуков, проксировании изображений или агрегации данных.

Защита на уровне сетевых примитивов:

Go
func isSafeURL(rawURL string) bool {
    u, err := url.Parse(rawURL)
    if err != nil { return false }

    // 🔒 Блокировка локальных и приватных диапазонов
    if ip := net.ParseIP(u.Hostname()); ip != nil {
        return !ip.IsLoopback() && !ip.IsPrivate() && !ip.IsLinkLocalUnicast()
    }
    // Разрешаем только http/https
    return u.Scheme == "http" || u.Scheme == "https"
}

// ✅ Клиент с отключенными редиректами и проверкой хоста
client := &http.Client{
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
        return http.ErrUseLastResponse // 🔒 Блокируем SSRF через 302
    },
    Timeout: 10 * time.Second,
}

Подробнее: 3. SSRF.


Mechanical Sympathy: Как уязвимости влияют на рантайм Go

Понимание того, как атаки воздействуют на уровень ОС и планировщик, помогает проектировать устойчивые системы:

flowchart TD
    A["Атака на приложение"] --> B["Высокая нагрузка CPU"]
    A --> C["Исчерпание памяти"]
    A --> D["Блокировка горутин"]

    B --> E["Паузы STW<br>GC перегрузка"]
    C --> F["OOM Killer<br>от ОС"]
    D --> G["Планировщик<br>создаёт треды M"]

    E --> H["Latency spikes<br>Timeout errors"]
    F --> I["Полный рестарт сервиса"]
    G --> J["Файловые дескрипторы исчерпаны"]

    style A fill:#7f1d1d,stroke:#f87171,color:#f8fafc
    style I fill:#7f1d1d,stroke:#f87171,color:#f8fafc
    style J fill:#7f1d1d,stroke:#f87171,color:#f8fafc
  
  1. JSON-бомбы и ReDoS → вызывают аллокацию миллионов мелких объектов. GC переходит в агрессивный режим, увеличивая GOGC, что приводит к паузам Stop-The-World. На уровне CPU кэш-линии постоянно вытесняются, пропускная способность падает.
  2. Медленные запросы (Slowloris) → горутины (g) блокируются в syscall read (вызов syscall.Read). Планировщик runtime видит, что P простаивает, и создаёт новые M (треды ОС). Лимит тредов GOMAXPROCS не спасает, так как треды создаются динамически. Результат: исчерпание ulimit -n (файловые дескрипторы) или vm.max_map_count.
  3. Memory Leaks в pprof → если не ограничить http.Server.MaxHeaderBytes или не закрывать r.Body, буферы bufio остаются в куче. GC не может их собрать, так как ссылки живы в net/http внутренностях.

Интеграция в процесс разработки

OWASP Top 10 должен быть встроен в CI/CD и код-ревью, а не проверяться перед релизом:

flowchart LR
    A["Код пуш"] --> B["gosec<br>Статический анализ"]
    B --> C["govulncheck<br>Проверка зависимостей"]
    C --> D["Интеграционные тесты<br>с Testcontainers"]
    D --> E["OWASP ZAP<br>DAST сканирование"]
    E --> F["Деплой"]

    style B fill:#132d29,stroke:#2dd4bf,color:#f8fafc
    style C fill:#132d29,stroke:#2dd4bf,color:#f8fafc
    style E fill:#78350f,stroke:#fbbf24,color:#f8fafc
  
  1. Pre-commit / CI: gosec -exclude=G104 -fmt=sarif ./... (подробнее в 2. Static analysis).
  2. Dependency Scan: govulncheck ./... проверяет go.sum против базы уязвимостей.
  3. DAST: Автоматический запуск OWASP ZAP или nuclei на тестовом окружении.
  4. Code Review: Чек-лист по OWASP Top 10 в шаблоне Pull Request.

Итог

OWASP Top 10 для бэкенд-разработчика на Go — это не список абстрактных угроз, а конкретные рекомендации по конфигурации net/http, работе с context, управлению памятью и валидации данных. Понимание механики рантайма позволяет предотвращать уязвимости на этапе проектирования, а не латать их постфактум.

В следующей лекции мы углубимся в один из самых фундаментальных дуализмов безопасности: 4. Аутентификация vs авторизация.