Цели модуля

После модуля студент сможет:

  • Применять OWASP Top 10 на практике в Go-сервисах
  • Различать authentication и authorization
  • Реализовывать HMAC signing и verification
  • Безопасно управлять секретами и их ротацией
  • Защищаться от SQL injection, XSS, CSRF
  • Правильно логировать без утечки PII
  • Настраивать HTTPS и security headers
  • Реализовывать rate limiting

9.1 OWASP Top 10 на практике

Что такое OWASP Top 10

OWASP (Open Web Application Security Project) публикует список 10 самых критичных уязвимостей web-приложений. Знание этих уязвимостей обязательно для backend-разработчика.

Top 10 уязвимостей (2021)

№УязвимостьЗащита в Go
A01Broken Access ControlRBAC, проверки прав в middleware
A02Cryptographic FailuresTLS, secure hashing, не хранить пароли
A03InjectionПараметризованные запросы, валидация
A04Insecure DesignThreat modeling, secure patterns
A05Security MisconfigurationМинимальные права, дефолтные пароли
A06Vulnerable Componentsgo mod tidy, govulncheck, Trivy
A07Auth FailuresJWT validation, rate limiting
A08Software and Data IntegritySBOM, sigstore, проверка checksum
A09Logging FailuresPII masking, audit logs
A10SSRFВалидация URL, allowlist

Как проверять безопасность в CI

bash
# govulncheck - проверка известных уязвимостей
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...

# gosec - статический анализ безопасности
go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec ./...

# Trivy - сканирование зависимостей и образов
trivy fs --security-checks vuln ./
trivy image myapp:latest

9.2 Authentication vs Authorization

Разница

Authentication (аутентификация) — кто ты? Authorization (авторизация) — что тебе можно?

Пример:

  • Authentication: логин по email+паролю, получение JWT токена
  • Authorization: проверка что пользователь имеет право создать репо

Middleware для authentication

go
func AuthMiddleware(tokenValidator TokenValidator) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            token := extractToken(r)
            if token == "" {
                writeProblem(w, http.StatusUnauthorized, "Unauthorized", "missing token")
                return
            }

            claims, err := tokenValidator.Validate(r.Context(), token)
            if err != nil {
                writeProblem(w, http.StatusUnauthorized, "Unauthorized", "invalid token")
                return
            }

            // Сохраняем пользователя в context
            ctx := context.WithValue(r.Context(), UserIDKey, claims.UserID)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Middleware для authorization

go
func RequireRole(role string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            userID := GetUserID(r.Context())

            user, err := userService.GetUser(r.Context(), userID)
            if err != nil {
                writeProblem(w, http.StatusInternalServerError, "Error", "failed to get user")
                return
            }

            if user.Role != role {
                writeProblem(w, http.StatusForbidden, "Forbidden", "insufficient permissions")
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

Проверка владения ресурсом

Частая ошибка: пользователь может изменить чужой ресурс.

go
// ПЛОХО: нет проверки владения
func (h *Handler) DeleteRepo(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    h.repoService.Delete(r.Context(), id)  // любой может удалить!
}

// ПРАВИЛЬНО: проверка владельца
func (h *Handler) DeleteRepo(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    userID := GetUserID(r.Context())

    repo, err := h.repoService.GetRepo(r.Context(), id)
    if err != nil {
        h.handleError(w, err)
        return
    }

    if repo.OwnerID != userID {
        writeProblem(w, http.StatusForbidden, "Forbidden", "not owner")
        return
    }

    h.repoService.Delete(r.Context(), id)
}

RBAC (Role-Based Access Control)

go
type Role string

const (
    RoleAdmin  Role = "admin"
    RoleUser   Role = "user"
    RoleViewer Role = "viewer"
)

type Permission struct {
    Resource string
    Action   string
}

var rolePermissions = map[Role][]Permission{
    RoleAdmin: {
        {Resource: "repos", Action: "create"},
        {Resource: "repos", Action: "read"},
        {Resource: "repos", Action: "update"},
        {Resource: "repos", Action: "delete"},
    },
    RoleUser: {
        {Resource: "repos", Action: "create"},
        {Resource: "repos", Action: "read"},
        {Resource: "repos", Action: "update"},
    },
    RoleViewer: {
        {Resource: "repos", Action: "read"},
    },
}

9.3 HMAC signing

Что такое HMAC

HMAC (Hash-based Message Authentication Code) — криптографическая конструкция для проверки целостности и подлинности сообщения.

Использует:

  • Секретный ключ (известен обеим сторонам)
  • Хеш-функцию (SHA-256, SHA-512)
  • Сообщение

Когда использовать HMAC

  • Webhooks (проверка что запрос от GitHub/GitLab)
  • API между сервисами (service-to-service)
  • Подпись payload в распределённых системах
  • Проверка целостности сообщений

Реализация в Go

go
package hmac

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
)

type Signer struct {
    secret []byte
}

func NewSigner(secret string) (*Signer, error) {
    if len(secret) < 32 {
        return nil, fmt.Errorf("secret must be at least 32 bytes")
    }
    return &Signer{secret: []byte(secret)}, nil
}

func (s *Signer) Sign(message []byte) string {
    mac := hmac.New(sha256.New, s.secret)
    mac.Write(message)
    return hex.EncodeToString(mac.Sum(nil))
}

Верификация подписи

go
type Verifier struct {
    secret []byte
}

func NewVerifier(secret string) (*Verifier, error) {
    if len(secret) < 32 {
        return nil, fmt.Errorf("secret must be at least 32 bytes")
    }
    return &Verifier{secret: []byte(secret)}, nil
}

func (v *Verifier) Verify(message []byte, signature string) error {
    mac := hmac.New(sha256.New, v.secret)
    mac.Write(message)
    expectedMAC := mac.Sum(nil)

    decoded, err := hex.DecodeString(signature)
    if err != nil {
        return fmt.Errorf("decode signature: %w", err)
    }

    // ConstantTimeCompare защищает от timing attacks
    if !hmac.Equal(decoded, expectedMAC) {
        return errors.New("invalid signature")
    }

    return nil
}

Использование для webhook verification

go
func (h *Handler) HandleWebhook(w http.ResponseWriter, r *http.Request) {
    // Читаем тело
    body, err := io.ReadAll(r.Body)
    if err != nil {
        writeProblem(w, http.StatusBadRequest, "Bad Request", "failed to read body")
        return
    }

    // Получаем подпись из заголовка
    signature := r.Header.Get("X-Signature-256")
    if signature == "" {
        writeProblem(w, http.StatusUnauthorized, "Unauthorized", "missing signature")
        return
    }

    // Проверяем подпись
    if err := h.verifier.Verify(body, signature); err != nil {
        writeProblem(w, http.StatusUnauthorized, "Unauthorized", "invalid signature")
        return
    }

    // Обрабатываем webhook
    // ...
}

Отправка подписанного запроса

go
func (c *Client) SendWebhook(ctx context.Context, url string, payload []byte) error {
    signature := c.signer.Sign(payload)

    req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload))
    if err != nil {
        return fmt.Errorf("create request: %w", err)
    }

    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-Signature-256", signature)
    req.Header.Set("X-Webhook-ID", generateID())
    req.Header.Set("X-Webhook-Timestamp", strconv.FormatInt(time.Now().Unix(), 10))

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return fmt.Errorf("send webhook: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 400 {
        return fmt.Errorf("webhook failed with status %d", resp.StatusCode)
    }

    return nil
}

Защита от replay attacks

Злоумышленник может перехватить валидный запрос и повторить. Защита: timestamp + nonce + idempotency key.

go
// При подписании добавляем timestamp
message := fmt.Sprintf("%s.%s.%d", webhookID, body, timestamp)
signature := signer.Sign([]byte(message))

// При верификации проверяем timestamp
func (v *Verifier) VerifyWithTimestamp(body []byte, signature string, webhookID string, timestamp int64) error {
    // Проверяем что timestamp не старше 5 минут
    if time.Now().Unix() - timestamp > 300 {
        return errors.New("timestamp too old")
    }

    message := fmt.Sprintf("%s.%s.%d", webhookID, body, timestamp)
    return v.Verify([]byte(message), signature)
}

9.4 Ротация секретов

Зачем нужна ротация

Секреты могут быть скомпрометированы:

  • Утечка через логи
  • Доступ бывшего сотрудника
  • Взлом системы
  • Компрометация секрет-менеджера

Ротация регулярно меняет секреты, снижая окно экспозиции.

Стратегия ротации HMAC ключей

Двухфазная ротация:

  1. Фаза 1: добавить новый ключ, принимать подписи обоими

  2. Фаза 2: прекратить использование старого ключа

    type MultiKeyVerifier struct { currentKey []byte previousKey []byte // опционально, для периода ротации }

    func (v *MultiKeyVerifier) Verify(message []byte, signature string) error { // Пробуем текущий ключ if err := verifyWithKey(v.currentKey, message, signature); err == nil { return nil }

     // Пробуем предыдущий ключ (период ротации)
     if v.previousKey != nil {
         if err := verifyWithKey(v.previousKey, message, signature); err == nil {
             return nil
         }
     }
    
     return errors.New("invalid signature")
    

    }

HMAC rotator job

В шаблоне проекта есть hmac-rotator cron job:

go
type HMACRotatorJob struct {
    secretProvider repository.SecretProvider
    notifier       Notifier
    logger         *slog.Logger
}

func (j *HMACRotatorJob) Run(ctx context.Context) error {
    // Проверяем возраст текущего ключа
    metadata, err := j.secretProvider.GetMetadata(ctx, "hmac-key")
    if err != nil {
        return fmt.Errorf("get metadata: %w", err)
    }

    if time.Since(metadata.CreatedAt) < RotationPeriod {
        return nil  // ещё рано
    }

    // Генерируем новый ключ
    newKey := generateSecureKey(64)

    // Сохраняем (с предыдущим ключом как previous)
    if err := j.secretProvider.Rotate(ctx, "hmac-key", newKey); err != nil {
        return fmt.Errorf("rotate: %w", err)
    }

    // Уведомляем команду
    j.notifier.Notify("HMAC key rotated", "New key active, previous valid for 24h")

    j.logger.Info("hmac key rotated")
    return nil
}

Периодичность ротации

СекретПериодичность
HMAC ключи для webhook30-90 дней
API ключи90 дней
TLS сертификаты90 дней (Let's Encrypt)
Пароли БД90 дней
OAuth токеныПо истечении срока жизни

9.5 Secrets management

Правила работы с секретами

  1. Никогда не коммитить секреты в git
  2. Использовать secret manager (Vault, Lockbox, AWS Secrets Manager)
  3. Передавать секреты через env в runtime
  4. Ротировать периодически
  5. Маскировать в логах
  6. Применять least privilege

Хранение секретов

go
// ПЛОХО: секрет в коде
const HMACSecret = "sk-1234567890abcdef"

// ПЛОХО: секрет в git
# config.prod.yaml
hmac_secret: "sk-1234567890abcdef"

// ПРАВИЛЬНО: секрет из env или secret manager
hmacSecret := os.Getenv("HMAC_SECRET")

Использование secret manager (Lockbox)

go
package lockbox

import "context"

type SecretProvider struct {
    client *lockbox.Client
}

func (p *SecretProvider) GetSecret(ctx context.Context, key string) (string, error) {
    secret, err := p.client.Get(ctx, key)
    if err != nil {
        return "", fmt.Errorf("get secret: %w", err)
    }
    return secret.Value, nil
}

func (p *SecretProvider) Rotate(ctx context.Context, key string, newValue string) error {
    return p.client.Update(ctx, key, newValue)
}

Загрузка секретов при старте

go
func loadSecrets(ctx context.Context, provider *lockbox.SecretProvider) (*Secrets, error) {
    hmacKey, err := provider.GetSecret(ctx, "hmac-key")
    if err != nil {
        return nil, fmt.Errorf("load hmac key: %w", err)
    }

    dbPassword, err := provider.GetSecret(ctx, "db-password")
    if err != nil {
        return nil, fmt.Errorf("load db password: %w", err)
    }

    return &Secrets{
        HMACKey:    hmacKey,
        DBPassword: dbPassword,
    }, nil
}

9.6 Input validation

SQL injection

Атака: внедрение SQL-кода через пользовательский ввод.

go
// УЯЗВИМО
query := "SELECT * FROM users WHERE email = '" + email + "'"
// email = "'; DROP TABLE users; --"

// БЕЗОПАСНО: параметризованный запрос
query := "SELECT * FROM users WHERE email = $1"
row := pool.QueryRow(ctx, query, email)

Правило: всегда использовать параметризованные запросы.

XSS (Cross-Site Scripting)

Атака: внедрение JavaScript через пользовательский ввод.

go
// УЯЗВИМО
fmt.Fprintf(w, "Hello, %s", userInput)
// userInput = "<script>alert('xss')</script>"

// БЕЗОПАСНО: экранирование HTML
import "html"
fmt.Fprintf(w, "Hello, %s", html.EscapeString(userInput))

Для backend API XSS менее критичен, но нужно экранировать при генерации HTML.

CSRF (Cross-Site Request Forgery)

Атака: выполнение действия от имени авторизованного пользователя.

Защита:

  • SameSite cookies (SameSite=Strict или Lax)
  • CSRF tokens для state-changing операций
  • Проверка Origin/Referer заголовков
  • Требовать Content-Type: application/json (браузеры не могут отправить)
go
// Установка SameSite cookie
http.SetCookie(w, &http.Cookie{
    Name:     "session",
    Value:    token,
    HttpOnly: true,
    Secure:   true,
    SameSite: http.SameSiteStrictMode,
})

Path traversal

Атака: доступ к файлам вне разрешённой директории.

go
// УЯЗВИМО
filePath := filepath.Join(baseDir, userInput)
// userInput = "../../etc/passwd"

// БЕЗОПАСНО: проверка что путь внутри baseDir
filePath := filepath.Join(baseDir, filepath.Clean(userInput))
if !strings.HasPrefix(filePath, baseDir) {
    return errors.New("invalid path")
}

Валидация URL для защиты от SSRF

go
func ValidateWebhookURL(rawURL string) error {
    u, err := url.Parse(rawURL)
    if err != nil {
        return errors.New("invalid url")
    }

    // Только HTTPS
    if u.Scheme != "https" {
        return errors.New("only https allowed")
    }

    // Запрет частных IP
    host := u.Hostname()
    ip := net.ParseIP(host)
    if ip != nil && ip.IsPrivate() {
        return errors.New("private IP not allowed")
    }

    // Запрет localhost
    if host == "localhost" || host == "127.0.0.1" {
        return errors.New("localhost not allowed")
    }

    return nil
}

9.7 Safe logging и PII

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

  • Пароли и секреты
  • Токены (JWT, API keys)
  • Email, телефоны в открытом виде
  • Платежные данные (PAN, CVV)
  • Тело запроса с PII
  • Заголовки Authorization, Cookie

Маскирование в логах

go
func maskSecret(s string) string {
    if len(s) <= 8 {
        return "***"
    }
    return s[:4] + "***" + s[len(s)-4:]
}

// Использование
logger.Info("api call",
    "api_key", maskSecret(apiKey),  // abcd***xyzw
)

Middleware для маскирования заголовков

go
func SanitizeHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Удаляем чувствительные заголовки перед логированием
        r.Header.Del("Authorization")
        r.Header.Del("Cookie")

        next.ServeHTTP(w, r)
    })
}

PII masking через slog handler

go
type PIIMaskingHandler struct {
    next slog.Handler
}

func (h *PIIMaskingHandler) Handle(ctx context.Context, r slog.Record) error {
    // Проходим по всем атрибутам и маскируем PII
    r.Attrs(func(a slog.Attr) bool {
        if isPIIField(a.Key) {
            // Заменяем значение на маскированное
            // ...
        }
        return true
    })

    return h.next.Handle(ctx, r)
}

Audit logging

Для критичных операций ведём audit log:

go
type AuditEvent struct {
    Timestamp time.Time
    UserID    string
    Action    string
    Resource  string
    Result    string
    IPAddress string
    UserAgent string
}

func (s *RepoService) DeleteRepo(ctx context.Context, id string) error {
    userID := middleware.GetUserID(ctx)
    err := s.repo.Delete(ctx, id)

    // Audit log независимо от результата
    s.auditLogger.Log(AuditEvent{
        Timestamp: time.Now(),
        UserID:    userID,
        Action:    "repo.delete",
        Resource:  id,
        Result:    errorToResult(err),
        IPAddress: middleware.GetIPAddress(ctx),
    })

    return err
}

9.8 Security headers и HTTPS

Обязательные security headers

go
func SecurityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("X-Frame-Options", "DENY")
        w.Header().Set("X-XSS-Protection", "1; mode=block")
        w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        w.Header().Set("Content-Security-Policy", "default-src 'self'")

        next.ServeHTTP(w, r)
    })
}

Значение заголовков

ЗаголовокНазначение
X-Content-Type-OptionsЗапрет MIME-sniffing
X-Frame-OptionsЗапрет встраивания в iframe
X-XSS-ProtectionЗащита от XSS в старых браузерах
Referrer-PolicyКонтроль referrer
Strict-Transport-SecurityПринудительный HTTPS (HSTS)
Content-Security-PolicyРазрешённые источники ресурсов

HTTPS и TLS

В production всегда использовать HTTPS. Для Go сервиса обычно TLS терминируется на load balancer или reverse proxy (nginx, Envoy, ALB).

Для internal communication:

  • mTLS (mutual TLS) между сервисами
  • TLS для подключения к БД
  • TLS для внешних API
go
// Пример TLS клиента
tlsConfig := &tls.Config{
    MinVersion: tls.VersionTLS12,
    RootCAs:    certPool,
}

transport := &http.Transport{
    TLSClientConfig: tlsConfig,
}

client := &http.Client{
    Transport: transport,
    Timeout:   30 * time.Second,
}

9.9 Rate limiting

Зачем нужен rate limiting

  • Защита от DoS-атак
  • Защита от brute force
  • Предотвращение злоупотреблений
  • Fair use API

Алгоритмы rate limiting

АлгоритмОписание
Fixed windowСчётчик сбрасывается каждый интервал
Sliding windowСкользящее окно для плавности
Token bucketТокены накапливаются, расходуются на запросы
Leaky bucketРавномерная скорость обработки

Token bucket реализация

go
package ratelimit

import (
    "sync"
    "time"
)

type TokenBucket struct {
    mu         sync.Mutex
    tokens     float64
    maxTokens  float64
    refillRate float64  // токенов в секунду
    lastRefill time.Time
}

func NewTokenBucket(maxTokens int, refillRate float64) *TokenBucket {
    return &TokenBucket{
        tokens:     float64(maxTokens),
        maxTokens:  float64(maxTokens),
        refillRate: refillRate,
        lastRefill: time.Now(),
    }
}

func (b *TokenBucket) Allow() bool {
    b.mu.Lock()
    defer b.mu.Unlock()

    now := time.Now()
    elapsed := now.Sub(b.lastRefill).Seconds()
    b.tokens += elapsed * b.refillRate
    if b.tokens > b.maxTokens {
        b.tokens = b.maxTokens
    }
    b.lastRefill = now

    if b.tokens >= 1 {
        b.tokens--
        return true
    }

    return false
}

Rate limit middleware

go
func RateLimit(limiter *TokenBucket) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !limiter.Allow() {
                w.Header().Set("Retry-After", "60")
                writeProblem(w, http.StatusTooManyRequests, "Too Many Requests", "rate limit exceeded")
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

Per-user rate limiting

go
type UserRateLimiter struct {
    mu       sync.Mutex
    limiters map[string]*TokenBucket
}

func (l *UserRateLimiter) Allow(userID string) bool {
    l.mu.Lock()
    limiter, ok := l.limiters[userID]
    if !ok {
        limiter = NewTokenBucket(100, 10)
        l.limiters[userID] = limiter
    }
    l.mu.Unlock()

    return limiter.Allow()
}

Для production использовать Redis-based rate limiter для распределённого лимитирования между инстансами.


Привязка к шаблону проекта

Где security в шаблоне

text
internal/security/
├── hmac/
│   ├── verifier.go         # HMAC verification
│   ├── signer.go           # HMAC signing
│   └── rotation.go         # Ротация ключей
└── policy/
    └── labels.go           # Security labels

internal/handlers/http/middleware/
└── pii.go                  # PII masking middleware

Что изучаем на примере шаблона

  1. HMAC signer и verifier
  2. Ротация ключей через cron job
  3. PII masking в логах
  4. Security policy и labels

Задание: изучите internal/security/

  • Как реализован HMAC?
  • Есть ли ротация ключей?
  • Какие security policies?
  • Как обрабатываются ошибки подписи?

💱 Компромиссы

  • JWT vs серверные сессии: JWT проверяется локально через TokenValidator без похода в БД, но отозвать токен до истечения срока жизни нельзя; сессия отзывается мгновенно, но каждый запрос оплачивается обращением к хранилищу.
  • Узкое окно timestamp vs задержанные webhooks: проверка «не старше 5 минут» надёжно режет replay attacks, но легитимный запрос, задержавшийся в пути, будет отклонён; широкое окно пропускает повторы и требует nonce с хранением.
  • Rate limit по IP vs по userID: лимит по IP защищает анонимные эндпоинты от brute force, но режет пользователей за общим NAT; UserRateLimiter честнее для fair use, но требует аутентификации и Redis-лимитера между инстансами.
  • Двухфазная ротация vs одномоментная замена: приём подписей обоими ключами (currentKey и previousKey) не ломает webhooks в полёте, но скомпрометированный старый ключ живёт ещё 24 часа; мгновенная замена сужает окно экспозиции, но рвёт интеграцию.
  • Подробные логи vs PII-маскирование: maskSecret и PIIMaskingHandler исключают утечки токенов и email в логи, но замаскированные значения уже не сопоставить при разборе инцидента; полные логи удобнее для аудита, но сами становятся источником утечки.

Практическое задание

Задание 1: HMAC Signer и Verifier

Реализуйте:

go
type Signer struct {
    secret []byte
}

func (s *Signer) Sign(message []byte) string
func (s *Signer) Verify(message []byte, signature string) error

Протестируйте:

  • Валидная подпись проходит
  • Изменённое сообщение не проходит
  • Неправильная подпись не проходит
  • ConstantTimeCompare для timing safety

Задание 2: Webhook verification middleware

Реализуйте middleware для проверки HMAC подписи webhook:

go
func WebhookAuth(verifier *hmac.Verifier) func(http.Handler) http.Handler

Middleware:

  • Читает тело запроса
  • Извлекает подпись из заголовка X-Signature-256
  • Проверяет подпись
  • Возвращает 401 при невалидной подписи

Задание 3: Auth middleware с JWT

Реализуйте JWT authentication middleware:

go
func JWTAuth(validator TokenValidator) func(http.Handler) http.Handler

Middleware:

  • Извлекает Bearer token из Authorization
  • Валидирует JWT
  • Сохраняет user ID в context
  • Возвращает 401 при невалидном токене

Задание 4: Role-based authorization

Реализуйте RequireRole middleware:

go
func RequireRole(role string) func(http.Handler) http.Handler

Middleware:

  • Получает user ID из context
  • Загружает пользователя
  • Проверяет роль
  • Возвращает 403 если нет прав

Задание 5: Security headers middleware

Реализуйте middleware с security headers:

go
func SecurityHeaders(next http.Handler) http.Handler

Добавьте:

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Strict-Transport-Security
  • Content-Security-Policy

Задание 6: Rate limiting

Реализуйте rate limiter middleware:

go
func RateLimit(requestsPerMinute int) func(http.Handler) http.Handler

Middleware:

  • Ограничивает запросы per IP или per user
  • Возвращает 429 при превышении
  • Добавляет Retry-After заголовок

Задание 7: PII masking

Реализуйте middleware для маскирования PII:

go
func PIIMasking(logger *slog.Logger) func(http.Handler) http.Handler

Middleware:

  • Логирует запросы
  • Маскирует email, телефоны в логах
  • Не логирует Authorization и Cookie заголовки

Задание 8: Security check в CI

Добавьте в Makefile:

makefile
security:
    govulncheck ./...
    gosec ./...

Запустите и исправьте найденные проблемы.


Quiz "Проверь себя"

1. Зачем нужна ротация секретов и как она проводится?
2. Какие уязвимости входят в самые критичные категории OWASP Top 10? — выберите все подходящие
3. Что из перечисленного защищает от CSRF? — выберите все подходящие
4. Как получатель проверит подпись, вычислив свой HMAC?
go
mac := hmac.New(sha256.New, secret)
mac.Write(payload)
expected := mac.Sum(nil)
5. Что нужно удалить из заголовков перед логированием HTTP-запроса?
6. Чем authorization отличается от authentication?
7. Как защититься от SQL injection?
8. Какой ответ вернёт rate limiter при превышении лимита?
9. Зачем нужен rate limiting и какие алгоритмы применяют?
10. Что уязвимо в этом коде и почему?
go
query := "SELECT * FROM repos WHERE name = '" + r.URL.Query().Get("name") + "'"
db.Exec(query)

✅ Итоги модуля

Студент должен:

  • Применять OWASP Top 10 на практике
  • Различать authentication и authorization
  • Реализовывать HMAC signing и verification
  • Безопасно управлять секретами
  • Защищаться от SQL injection, XSS, CSRF
  • Правильно логировать без PII
  • Настраивать security headers
  • Реализовывать rate limiting
  • Ответить на все вопросы Quiz

🔗 Мостик к Модулю 10

Следующий модуль: Модуль 10. Docker и контейнеризация - multi-stage Dockerfile, Docker Compose, best practices (non-root, slim images), health checks, security scanning (Trivy). Привязка к docker/ шаблона.