Hard💻Практика18 min

Защита от злоупотреблений

WAF, защита от ботов, DDoS mitigation, CAPTCHA, brute force protection и account takeover prevention

Защита от злоупотреблений

Современные веб-приложения подвергаются постоянным атакам: от массовых DDoS до тонких попыток credential stuffing. Защита — это многоуровневая система, где каждый слой отсеивает определённый тип угроз.

Ландшафт угроз

Тип атаки Цель Сложность Ущерб
DDoS (L3/L4) Перегрузить сеть/инфраструктуру Низкая (booter-сервисы) Полная недоступность
DDoS (L7) Перегрузить приложение запросами Средняя Деградация/недоступность
Credential stuffing Подбор логин/пароль из утечек Низкая Account takeover
Brute force Перебор паролей Низкая Account takeover
Web scraping Массовое извлечение данных Средняя Потеря данных, нагрузка
Spam Засорение форм, комментариев Низкая Деградация UX
API abuse Превышение лимитов API Средняя Финансовые потери
Bot fraud Автоматизация действий (click fraud, fake accounts) Высокая Финансовые потери
Атаки по уровням:

┌─────────────────────────────────────────┐
│          Application Layer (L7)         │
│  Credential stuffing, scraping, spam    │
│  API abuse, bot fraud, brute force      │
├─────────────────────────────────────────┤
│         Transport Layer (L4)            │
│  SYN flood, TCP exhaustion             │
├─────────────────────────────────────────┤
│          Network Layer (L3)             │
│  UDP flood, ICMP flood, amplification   │
└─────────────────────────────────────────┘

Rate Limiting: многоуровневая защита

Rate limiting — первая линия обороны. Он должен работать на каждом уровне инфраструктуры.

                    ┌──────────────┐
  Запрос ──────────►│  CloudFlare  │  Cloud Level (L3-L7)
                    │   AWS WAF    │  Global rate limits
                    └──────┬───────┘
                           │
                    ┌──────▼───────┐
                    │    Nginx     │  Infrastructure Level
                    │     ALB      │  Per-IP rate limits
                    └──────┬───────┘
                           │
                    ┌──────▼───────┐
                    │  Application │  Application Level
                    │  Middleware   │  Per-user, per-endpoint
                    └──────────────┘

Application Level

На уровне приложения мы контролируем rate limiting с максимальной гранулярностью — по пользователю, по эндпоинту, по типу операции.

Symfony RateLimiterGo Middleware
<?php

declare(strict_types=1);

namespace App\RateLimiter;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\RateLimiter\RateLimiterFactory;

final readonly class ApiRateLimitListener
{
    public function __construct(
        private RateLimiterFactory $anonymousApiLimiter,
        private RateLimiterFactory $authenticatedApiLimiter,
    ) {}

    public function __invoke(RequestEvent $event): void
    {
        $request = $event->getRequest();

        if (!str_starts_with($request->getPathInfo(), '/api/')) {
            return;
        }

        $limiter = $this->resolveLimiter($request);
        $limit = $limiter->consume();

        if (!$limit->isAccepted()) {
            $response = new Response('Too Many Requests', 429);
            $response->headers->set('Retry-After', (string) $limit->getRetryAfter()->getTimestamp());
            $response->headers->set('X-RateLimit-Limit', (string) $limit->getLimit());
            $response->headers->set('X-RateLimit-Remaining', '0');
            $event->setResponse($response);
        }
    }

    private function resolveLimiter(Request $request): \Symfony\Component\RateLimiter\RateLimiterInterface
    {
        $userId = $request->attributes->get('_user_id');

        if ($userId !== null) {
            return $this->authenticatedApiLimiter->create((string) $userId);
        }

        return $this->anonymousApiLimiter->create(
            $request->getClientIp() ?? 'unknown'
        );
    }
}
package middleware

import (
	"net/http"
	"sync"
	"time"

	"golang.org/x/time/rate"
)

// RateLimiter provides per-key rate limiting.
type RateLimiter struct {
	mu       sync.Mutex
	limiters map[string]*clientLimiter
	rate     rate.Limit
	burst    int
	ttl      time.Duration
}

type clientLimiter struct {
	limiter  *rate.Limiter
	lastSeen time.Time
}

// NewRateLimiter creates a rate limiter with the given requests/second and burst.
func NewRateLimiter(rps float64, burst int) *RateLimiter {
	rl := &RateLimiter{
		limiters: make(map[string]*clientLimiter),
		rate:     rate.Limit(rps),
		burst:    burst,
		ttl:      10 * time.Minute,
	}

	go rl.cleanup()

	return rl
}

// Allow checks if a request from the given key is allowed.
func (rl *RateLimiter) Allow(key string) bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()

	cl, exists := rl.limiters[key]
	if !exists {
		cl = &clientLimiter{
			limiter: rate.NewLimiter(rl.rate, rl.burst),
		}
		rl.limiters[key] = cl
	}

	cl.lastSeen = time.Now()

	return cl.limiter.Allow()
}

// Middleware wraps an HTTP handler with rate limiting.
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		key := extractClientIP(r)

		if !rl.Allow(key) {
			w.Header().Set("Retry-After", "60")
			http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func (rl *RateLimiter) cleanup() {
	ticker := time.NewTicker(5 * time.Minute)
	defer ticker.Stop()

	for range ticker.C {
		rl.mu.Lock()
		for key, cl := range rl.limiters {
			if time.Since(cl.lastSeen) > rl.ttl {
				delete(rl.limiters, key)
			}
		}
		rl.mu.Unlock()
	}
}

func extractClientIP(r *http.Request) string {
	if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
		return xff
	}
	return r.RemoteAddr
}
### Infrastructure Level (nginx)
# /etc/nginx/conf.d/rate_limit.conf

# Zone for general API requests: 10 req/s per IP
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

# Zone for login attempts: 5 req/min per IP
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

# Zone for registration: 3 req/min per IP
limit_req_zone $binary_remote_addr zone=register:10m rate=3r/m;

server {
    location /api/ {
        limit_req zone=api burst=20 nodelay;
        limit_req_status 429;
        proxy_pass http://backend;
    }

    location /api/auth/login {
        limit_req zone=login burst=3 nodelay;
        limit_req_status 429;
        proxy_pass http://backend;
    }

    location /api/auth/register {
        limit_req zone=register burst=2 nodelay;
        limit_req_status 429;
        proxy_pass http://backend;
    }
}

WAF (Web Application Firewall)

WAF анализирует HTTP-запросы и блокирует вредоносные на основе правил. Он работает между клиентом и приложением, фильтруя SQL injection, XSS, path traversal и другие атаки.

                  ┌────────────────┐
  Клиент ────────►│      WAF       │────── Разрешено ──────► Приложение
                  │                │
                  │  Правила:      │
                  │  - SQL Injection│────── Заблокировано ──► 403 Forbidden
                  │  - XSS         │
                  │  - Bot Control │
                  │  - Geo-block   │
                  │  - IP reputation│
                  └────────────────┘

AWS WAF: конфигурация через Terraform

# AWS WAF Web ACL attached to ALB
resource "aws_wafv2_web_acl" "main" {
  name        = "app-waf"
  description = "Main WAF for application"
  scope       = "REGIONAL"

  default_action {
    allow {}
  }

  # Rule 1: AWS Managed Common Rule Set (SQL injection, XSS, etc.)
  rule {
    name     = "AWSManagedRulesCommonRuleSet"
    priority = 1

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"
      }
    }

    visibility_config {
      sampled_requests_enabled   = true
      cloudwatch_metrics_enabled = true
      metric_name                = "CommonRuleSetMetric"
    }
  }

  # Rule 2: Bot Control
  rule {
    name     = "AWSBotControl"
    priority = 2

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesBotControlRuleSet"
        vendor_name = "AWS"

        managed_rule_group_configs {
          aws_managed_rules_bot_control_rule_set {
            inspection_level = "COMMON"
          }
        }
      }
    }

    visibility_config {
      sampled_requests_enabled   = true
      cloudwatch_metrics_enabled = true
      metric_name                = "BotControlMetric"
    }
  }

  # Rule 3: Geo-blocking (block specific countries)
  rule {
    name     = "GeoBlock"
    priority = 3

    action {
      block {}
    }

    statement {
      geo_match_statement {
        country_codes = ["CN", "RU", "KP"]  # Adjust per business requirements
      }
    }

    visibility_config {
      sampled_requests_enabled   = true
      cloudwatch_metrics_enabled = true
      metric_name                = "GeoBlockMetric"
    }
  }

  # Rule 4: Rate limiting per IP
  rule {
    name     = "RateLimitPerIP"
    priority = 4

    action {
      block {}
    }

    statement {
      rate_based_statement {
        limit              = 2000
        aggregate_key_type = "IP"
      }
    }

    visibility_config {
      sampled_requests_enabled   = true
      cloudwatch_metrics_enabled = true
      metric_name                = "RateLimitMetric"
    }
  }

  visibility_config {
    sampled_requests_enabled   = true
    cloudwatch_metrics_enabled = true
    metric_name                = "MainWAFMetric"
  }
}

# Associate WAF with ALB
resource "aws_wafv2_web_acl_association" "main" {
  resource_arn = aws_lb.main.arn
  web_acl_arn  = aws_wafv2_web_acl.main.arn
}

Ключевые managed rule groups AWS WAF

Rule Group Защита от
AWSManagedRulesCommonRuleSet OWASP Top 10 (SQLi, XSS, LFI, RFI)
AWSManagedRulesKnownBadInputsRuleSet Известные вредоносные паттерны
AWSManagedRulesSQLiRuleSet SQL injection (расширенные правила)
AWSManagedRulesLinuxRuleSet LFI для Linux серверов
AWSManagedRulesBotControlRuleSet Бот-трафик (категоризация)
AWSManagedRulesATPRuleSet Account takeover prevention

Bot Detection

Боты генерируют до 40% интернет-трафика. Задача — отличить легитимных ботов (Googlebot, мониторинг) от вредоносных (скраперы, credential stuffing).

Уровни защиты от ботов

Уровень Метод Эффективность Ложные срабатывания
Базовый User-Agent фильтрация Низкая (~10%) Низкие
Средний JavaScript challenge Средняя (~60%) Средние
Средний Browser fingerprinting Средняя (~70%) Средние
Продвинутый Поведенческий анализ Высокая (~90%) Низкие
Продвинутый ML-модели Очень высокая (~95%) Очень низкие

Honeypot: скрытые ловушки для ботов

Honeypot — это скрытое поле формы, невидимое обычному пользователю, но заполняемое ботами. Простой и эффективный метод.

┌──────────────────────────────┐
│          Форма               │
│                              │
│  Email: [____________]       │
│  Password: [____________]    │
│                              │
│  ┌─────────────────────┐     │
│  │ Hidden field (CSS)  │     │  ← Бот заполнит это поле
│  │ website: [________] │     │  ← Человек не увидит
│  └─────────────────────┘     │
│                              │
│  [Submit]                    │
└──────────────────────────────┘
PHP HoneypotGo Honeypot Middleware
<?php

declare(strict_types=1);

namespace App\Security;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;

final readonly class HoneypotValidator
{
    private const string HONEYPOT_FIELD = 'website_url';
    private const string HONEYPOT_TIMESTAMP = '_form_ts';
    private const int MIN_SUBMISSION_TIME_SECONDS = 3;

    public function __construct(
        private LoggerInterface $logger,
    ) {}

    /**
     * Validate that the request is not from a bot.
     * Returns true if the request looks legitimate.
     */
    public function validate(Request $request): bool
    {
        // Check 1: Honeypot field must be empty
        $honeypotValue = $request->request->get(self::HONEYPOT_FIELD, '');

        if ($honeypotValue !== '') {
            $this->logger->warning('Bot detected: honeypot field filled', [
                'ip' => $request->getClientIp(),
                'user_agent' => $request->headers->get('User-Agent'),
                'honeypot_value' => $honeypotValue,
            ]);

            return false;
        }

        // Check 2: Form must not be submitted too quickly (< 3 seconds)
        $formTimestamp = $request->request->get(self::HONEYPOT_TIMESTAMP);

        if ($formTimestamp !== null) {
            $elapsed = time() - (int) $formTimestamp;

            if ($elapsed < self::MIN_SUBMISSION_TIME_SECONDS) {
                $this->logger->warning('Bot detected: form submitted too quickly', [
                    'ip' => $request->getClientIp(),
                    'elapsed_seconds' => $elapsed,
                ]);

                return false;
            }
        }

        return true;
    }

    /**
     * Generate honeypot fields for a form.
     * @return array{field_name: string, timestamp_name: string, timestamp_value: int}
     */
    public function generateFields(): array
    {
        return [
            'field_name' => self::HONEYPOT_FIELD,
            'timestamp_name' => self::HONEYPOT_TIMESTAMP,
            'timestamp_value' => time(),
        ];
    }
}
package middleware

import (
	"log/slog"
	"net/http"
	"strconv"
	"time"
)

const (
	honeypotField         = "website_url"
	honeypotTimestamp      = "_form_ts"
	minSubmissionTimeSec   = 3
)

// HoneypotMiddleware rejects requests that fill hidden honeypot fields.
func HoneypotMiddleware(logger *slog.Logger, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			next.ServeHTTP(w, r)
			return
		}

		if err := r.ParseForm(); err != nil {
			http.Error(w, "Bad Request", http.StatusBadRequest)
			return
		}

		// Check honeypot field — must be empty
		if r.FormValue(honeypotField) != "" {
			logger.Warn("bot detected: honeypot field filled",
				slog.String("ip", r.RemoteAddr),
				slog.String("user_agent", r.UserAgent()),
			)
			// Return 200 to not reveal detection
			w.WriteHeader(http.StatusOK)
			return
		}

		// Check submission timing — must be >= 3 seconds
		if tsStr := r.FormValue(honeypotTimestamp); tsStr != "" {
			ts, err := strconv.ParseInt(tsStr, 10, 64)
			if err == nil {
				elapsed := time.Now().Unix() - ts
				if elapsed < minSubmissionTimeSec {
					logger.Warn("bot detected: form submitted too quickly",
						slog.String("ip", r.RemoteAddr),
						slog.Int64("elapsed_sec", elapsed),
					)
					w.WriteHeader(http.StatusOK)
					return
				}
			}
		}

		next.ServeHTTP(w, r)
	})
}
### Поведенческий анализ

Продвинутые системы анализируют паттерны поведения:

Сигнал Человек Бот
Движения мыши Хаотичные, плавные кривые Прямые линии или отсутствуют
Скорость заполнения форм 5-30 секунд < 1 секунды
Скроллинг Неравномерный Отсутствует или линейный
Паттерн навигации Непоследовательный Систематический обход
Время между запросами Переменное Фиксированные интервалы
JavaScript execution Нормальный Headless browser или отсутствует
Cookie/localStorage Принимает Может не поддерживать

DDoS Mitigation

DDoS-атаки нацелены на перегрузку ресурсов. Защита строится послойно — каждый уровень фильтрует свой тип трафика.

                        ┌─────────────────────────────────────┐
                        │         Anycast DNS (CloudFlare)     │
                        │   Распределение трафика по PoP       │
                        │   L3/L4 scrubbing (UDP/SYN flood)    │
                        └──────────────┬──────────────────────┘
                                       │ Clean traffic
                        ┌──────────────▼──────────────────────┐
                        │         AWS Shield / CloudFlare       │
                        │   L3/L4: volumetric attack mitigation│
                        │   Auto-detection, always-on           │
                        └──────────────┬──────────────────────┘
                                       │
                        ┌──────────────▼──────────────────────┐
                        │         WAF (L7 protection)          │
                        │   HTTP flood detection                │
                        │   Bot filtering, rate limiting        │
                        │   Custom rules per endpoint           │
                        └──────────────┬──────────────────────┘
                                       │
                        ┌──────────────▼──────────────────────┐
                        │         Application                   │
                        │   Per-user rate limiting              │
                        │   Circuit breakers                    │
                        │   Auto-scaling (if legitimate spike)  │
                        └───────────────────────────────────────┘

L3/L4 DDoS (Volumetric)

Атаки на сетевом/транспортном уровне — UDP flood, SYN flood, amplification.

Решение Тип Защита
CloudFlare Cloud proxy Anycast, scrubbing centers, 300+ Tbps capacity
AWS Shield Standard Cloud (бесплатно) Автоматическая L3/L4 защита для всех AWS ресурсов
AWS Shield Advanced Cloud (платно) DDoS response team, финансовая защита, WAF credits
Anycast DNS Инфраструктура Распределение нагрузки по географическим точкам

L7 DDoS (Application)

Атаки на уровне приложения — HTTP flood, slowloris, API abuse.

Метод защиты Как работает
WAF rules Паттерны вредоносных HTTP-запросов
Rate limiting Ограничение запросов per IP / per session
JavaScript challenge Проверка наличия настоящего браузера
CAPTCHA При подозрительной активности
Auto-scaling Легитимный трафик — масштабируемся
Geographic blocking Блокировка регионов-источников атаки

Brute Force Protection

Brute force — перебор паролей. Защита комбинирует ограничения по IP и по аккаунту.

Попытка логина
    │
    ▼
┌───────────────────┐     Заблокирован    ┌─────────────────┐
│  IP Rate Limiter  │────────────────────►│  429 Too Many   │
│  (5 req/min)      │                     │  Requests       │
└───────┬───────────┘                     └─────────────────┘
        │ OK
        ▼
┌───────────────────┐     Заблокирован    ┌─────────────────┐
│ Account Throttler │────────────────────►│  423 Locked     │
│ (progressive)     │                     │  + Email alert  │
└───────┬───────────┘                     └─────────────────┘
        │ OK
        ▼
┌───────────────────┐     Неверный пароль ┌─────────────────┐
│ Auth Check        │────────────────────►│  401 + Counter  │
└───────┬───────────┘                     └─────────────────┘
        │ OK
        ▼
   Успешный вход

Progressive Delays (прогрессивная задержка)

Попытка Задержка Действие
1-3 0 сек Нормальный ответ
4-5 2 сек Искусственная задержка
6-7 5 сек Задержка + CAPTCHA
8-9 15 сек Задержка + CAPTCHA + уведомление
10+ Блокировка Временная блокировка 30 мин + email
PHP LoginThrottlerGo LoginThrottler
<?php

declare(strict_types=1);

namespace App\Security;

use Psr\Log\LoggerInterface;

final class LoginThrottler
{
    private const int MAX_ATTEMPTS_BY_IP = 20;
    private const int MAX_ATTEMPTS_BY_ACCOUNT = 10;
    private const int LOCKOUT_DURATION_SECONDS = 1800; // 30 minutes
    private const string PREFIX_IP = 'login_throttle:ip:';
    private const string PREFIX_ACCOUNT = 'login_throttle:account:';

    /** @var array<int, int> Attempt number => delay in seconds */
    private const array PROGRESSIVE_DELAYS = [
        4 => 2,
        6 => 5,
        8 => 15,
    ];

    public function __construct(
        private readonly \Redis $redis,
        private readonly LoggerInterface $logger,
    ) {}

    /**
     * Check if the login attempt should be allowed.
     * Returns the delay in seconds (0 = no delay, -1 = blocked).
     */
    public function checkAttempt(string $ip, string $email): int
    {
        // Check IP-based throttling
        $ipKey = self::PREFIX_IP . $ip;
        $ipAttempts = (int) $this->redis->get($ipKey);

        if ($ipAttempts >= self::MAX_ATTEMPTS_BY_IP) {
            $this->logger->warning('Login blocked: IP rate limit exceeded', [
                'ip' => $ip,
                'attempts' => $ipAttempts,
            ]);

            return -1;
        }

        // Check account-based throttling
        $accountKey = self::PREFIX_ACCOUNT . $this->hashEmail($email);
        $accountAttempts = (int) $this->redis->get($accountKey);

        if ($accountAttempts >= self::MAX_ATTEMPTS_BY_ACCOUNT) {
            $this->logger->warning('Login blocked: account locked', [
                'email' => $this->maskEmail($email),
                'attempts' => $accountAttempts,
            ]);

            return -1;
        }

        // Calculate progressive delay
        return $this->calculateDelay($accountAttempts);
    }

    /**
     * Record a failed login attempt.
     */
    public function recordFailure(string $ip, string $email): void
    {
        $ipKey = self::PREFIX_IP . $ip;
        $this->redis->incr($ipKey);
        $this->redis->expire($ipKey, self::LOCKOUT_DURATION_SECONDS);

        $accountKey = self::PREFIX_ACCOUNT . $this->hashEmail($email);
        $attempts = $this->redis->incr($accountKey);
        $this->redis->expire($accountKey, self::LOCKOUT_DURATION_SECONDS);

        if ($attempts >= self::MAX_ATTEMPTS_BY_ACCOUNT) {
            $this->logger->alert('Account locked due to brute force attempts', [
                'email' => $this->maskEmail($email),
                'ip' => $ip,
            ]);
            // TODO: send notification email to the user
        }
    }

    /**
     * Reset throttle counters on successful login.
     */
    public function recordSuccess(string $ip, string $email): void
    {
        $this->redis->del(self::PREFIX_IP . $ip);
        $this->redis->del(self::PREFIX_ACCOUNT . $this->hashEmail($email));
    }

    private function calculateDelay(int $attempts): int
    {
        foreach (array_reverse(self::PROGRESSIVE_DELAYS, true) as $threshold => $delay) {
            if ($attempts >= $threshold) {
                return $delay;
            }
        }

        return 0;
    }

    private function hashEmail(string $email): string
    {
        return hash('sha256', mb_strtolower($email));
    }

    private function maskEmail(string $email): string
    {
        $parts = explode('@', $email);

        if (count($parts) !== 2) {
            return '***';
        }

        $name = $parts[0];
        $masked = mb_substr($name, 0, 2) . '***';

        return $masked . '@' . $parts[1];
    }
}
package auth

import (
	"context"
	"crypto/sha256"
	"fmt"
	"log/slog"
	"time"

	"github.com/redis/go-redis/v9"
)

const (
	maxAttemptsByIP      = 20
	maxAttemptsByAccount = 10
	lockoutDuration      = 30 * time.Minute
	prefixIP             = "login_throttle:ip:"
	prefixAccount        = "login_throttle:account:"
)

// progressiveDelays maps attempt count thresholds to delay in seconds.
var progressiveDelays = []struct {
	threshold int
	delay     int
}{
	{8, 15},
	{6, 5},
	{4, 2},
}

// LoginThrottler provides brute-force protection for login endpoints.
type LoginThrottler struct {
	rdb    *redis.Client
	logger *slog.Logger
}

// NewLoginThrottler creates a new throttler instance.
func NewLoginThrottler(rdb *redis.Client, logger *slog.Logger) *LoginThrottler {
	return &LoginThrottler{rdb: rdb, logger: logger}
}

// CheckAttempt returns the delay in seconds (0 = no delay, -1 = blocked).
func (t *LoginThrottler) CheckAttempt(ctx context.Context, ip, email string) (int, error) {
	// Check IP-based throttling
	ipKey := prefixIP + ip
	ipAttempts, err := t.rdb.Get(ctx, ipKey).Int()
	if err != nil && err != redis.Nil {
		return 0, fmt.Errorf("check IP attempts: %w", err)
	}

	if ipAttempts >= maxAttemptsByIP {
		t.logger.Warn("login blocked: IP rate limit exceeded",
			slog.String("ip", ip),
			slog.Int("attempts", ipAttempts),
		)
		return -1, nil
	}

	// Check account-based throttling
	accountKey := prefixAccount + hashEmail(email)
	accountAttempts, err := t.rdb.Get(ctx, accountKey).Int()
	if err != nil && err != redis.Nil {
		return 0, fmt.Errorf("check account attempts: %w", err)
	}

	if accountAttempts >= maxAttemptsByAccount {
		t.logger.Warn("login blocked: account locked",
			slog.String("email", maskEmail(email)),
			slog.Int("attempts", accountAttempts),
		)
		return -1, nil
	}

	return calculateDelay(accountAttempts), nil
}

// RecordFailure increments failure counters for IP and account.
func (t *LoginThrottler) RecordFailure(ctx context.Context, ip, email string) error {
	pipe := t.rdb.Pipeline()

	ipKey := prefixIP + ip
	pipe.Incr(ctx, ipKey)
	pipe.Expire(ctx, ipKey, lockoutDuration)

	accountKey := prefixAccount + hashEmail(email)
	pipe.Incr(ctx, accountKey)
	pipe.Expire(ctx, accountKey, lockoutDuration)

	results, err := pipe.Exec(ctx)
	if err != nil {
		return fmt.Errorf("record failure: %w", err)
	}

	// Check if account is now locked (3rd command is Incr for account)
	if len(results) >= 3 {
		if attempts, ok := results[2].(*redis.IntCmd); ok {
			if val, _ := attempts.Result(); val >= maxAttemptsByAccount {
				t.logger.Error("account locked due to brute force",
					slog.String("email", maskEmail(email)),
					slog.String("ip", ip),
				)
			}
		}
	}

	return nil
}

// RecordSuccess resets throttle counters on successful login.
func (t *LoginThrottler) RecordSuccess(ctx context.Context, ip, email string) error {
	pipe := t.rdb.Pipeline()
	pipe.Del(ctx, prefixIP+ip)
	pipe.Del(ctx, prefixAccount+hashEmail(email))
	_, err := pipe.Exec(ctx)

	return err
}

func calculateDelay(attempts int) int {
	for _, pd := range progressiveDelays {
		if attempts >= pd.threshold {
			return pd.delay
		}
	}

	return 0
}

func hashEmail(email string) string {
	h := sha256.Sum256([]byte(email))
	return fmt.Sprintf("%x", h)
}

func maskEmail(email string) string {
	for i, c := range email {
		if c == '@' && i >= 2 {
			return email[:2] + "***" + email[i:]
		}
	}
	return "***"
}
## Credential Stuffing Prevention

Credential stuffing — массовая проверка пар логин/пароль из украденных баз данных. В отличие от brute force, используются реальные учётные данные из утечек.

Методы защиты

Метод Описание
HIBP API check Проверка пароля по базе утечек при регистрации
Impossible travel detection Логин из NY, через 5 минут из Лондона
Device fingerprinting Уведомление о входе с нового устройства
Credential monitoring Мониторинг утечек на появление email-ов пользователей
MFA Второй фактор делает украденный пароль бесполезным

Проверка пароля через Have I Been Pwned (HIBP)

Пароль: "MyPassword123"
    │
    ▼ SHA-1
Hash: "CBFDAC6008F9CAB4083784CBD1874F76618D2A97"
    │
    ▼ Отправляем первые 5 символов
GET https://api.pwnedpasswords.com/range/CBFDA
    │
    ▼ API возвращает ~500 суффиксов
C6008F9CAB4083784CBD1874F76618D2A97:12345   ← Совпадение! Пароль скомпрометирован
A1B2C3D4E5F6...                             ← Не совпадает

Техника k-anonymity: API никогда не получает полный хеш, только первые 5 символов.

Impossible Travel Detection

PHPGo
<?php

declare(strict_types=1);

namespace App\Security;

final readonly class ImpossibleTravelDetector
{
    // Maximum plausible speed in km/h (commercial flight speed)
    private const float MAX_SPEED_KMH = 900.0;

    /**
     * Check if travel between two logins is physically impossible.
     *
     * @param array{lat: float, lon: float, time: \DateTimeImmutable} $lastLogin
     * @param array{lat: float, lon: float, time: \DateTimeImmutable} $currentLogin
     */
    public function isImpossibleTravel(array $lastLogin, array $currentLogin): bool
    {
        $distanceKm = $this->haversineDistance(
            $lastLogin['lat'],
            $lastLogin['lon'],
            $currentLogin['lat'],
            $currentLogin['lon'],
        );

        $timeDiffHours = ($currentLogin['time']->getTimestamp() - $lastLogin['time']->getTimestamp()) / 3600;

        if ($timeDiffHours <= 0) {
            return true;
        }

        $requiredSpeedKmh = $distanceKm / $timeDiffHours;

        return $requiredSpeedKmh > self::MAX_SPEED_KMH;
    }

    /**
     * Calculate distance between two points using haversine formula.
     */
    private function haversineDistance(
        float $lat1,
        float $lon1,
        float $lat2,
        float $lon2,
    ): float {
        $earthRadiusKm = 6371.0;

        $dLat = deg2rad($lat2 - $lat1);
        $dLon = deg2rad($lon2 - $lon1);

        $a = sin($dLat / 2) ** 2
            + cos(deg2rad($lat1)) * cos(deg2rad($lat2))
            * sin($dLon / 2) ** 2;

        $c = 2 * atan2(sqrt($a), sqrt(1 - $a));

        return $earthRadiusKm * $c;
    }
}
package security

import (
	"math"
	"time"
)

const (
	earthRadiusKm = 6371.0
	maxSpeedKmH   = 900.0 // Commercial flight speed
)

// LoginLocation represents a login event with geo data.
type LoginLocation struct {
	Lat  float64
	Lon  float64
	Time time.Time
}

// IsImpossibleTravel checks if travel between two logins is physically impossible.
func IsImpossibleTravel(last, current LoginLocation) bool {
	distance := haversineDistance(last.Lat, last.Lon, current.Lat, current.Lon)

	timeDiff := current.Time.Sub(last.Time).Hours()
	if timeDiff <= 0 {
		return true
	}

	requiredSpeed := distance / timeDiff

	return requiredSpeed > maxSpeedKmH
}

func haversineDistance(lat1, lon1, lat2, lon2 float64) float64 {
	dLat := degreesToRadians(lat2 - lat1)
	dLon := degreesToRadians(lon2 - lon1)

	a := math.Sin(dLat/2)*math.Sin(dLat/2) +
		math.Cos(degreesToRadians(lat1))*math.Cos(degreesToRadians(lat2))*
			math.Sin(dLon/2)*math.Sin(dLon/2)

	c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))

	return earthRadiusKm * c
}

func degreesToRadians(deg float64) float64 {
	return deg * math.Pi / 180
}
## CAPTCHA Integration

CAPTCHA нужна не везде — только при подозрительном поведении. Излишнее использование CAPTCHA ухудшает UX.

Когда показывать CAPTCHA

Ситуация Действие
3+ неудачных попыток логина Показать CAPTCHA
Массовая отправка форм Показать CAPTCHA
Подозрительный fingerprint Показать CAPTCHA
Новая регистрация Показать CAPTCHA
Обычный авторизованный пользователь Не показывать
Пользователь с историей Не показывать

Сравнение решений

Характеристика reCAPTCHA v3 hCaptcha
Тип Score-based (invisible) Challenge-based
Privacy Данные идут в Google Privacy-focused
Стоимость Бесплатно (до лимита) Бесплатно + платно
Accuracy Высокая Высокая
Accessibility Хорошая Хорошая
GDPR Требует consent Совместим

PHP: сервис верификации CAPTCHA

<?php

declare(strict_types=1);

namespace App\Security;

use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

final readonly class CaptchaVerifier
{
    private const string RECAPTCHA_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
    private const float MIN_SCORE = 0.5;

    public function __construct(
        private HttpClientInterface $httpClient,
        private string $recaptchaSecret,
        private LoggerInterface $logger,
    ) {}

    /**
     * Verify reCAPTCHA v3 token.
     * Returns score (0.0 - 1.0) or null on failure.
     */
    public function verify(string $token, string $expectedAction, ?string $ip = null): ?float
    {
        try {
            $response = $this->httpClient->request('POST', self::RECAPTCHA_VERIFY_URL, [
                'body' => [
                    'secret' => $this->recaptchaSecret,
                    'response' => $token,
                    'remoteip' => $ip,
                ],
            ]);

            /** @var array{success: bool, score?: float, action?: string, error-codes?: list<string>} $data */
            $data = $response->toArray();

            if (!$data['success']) {
                $this->logger->warning('CAPTCHA verification failed', [
                    'errors' => $data['error-codes'] ?? [],
                ]);

                return null;
            }

            // Verify action matches expected
            if (isset($data['action']) && $data['action'] !== $expectedAction) {
                $this->logger->warning('CAPTCHA action mismatch', [
                    'expected' => $expectedAction,
                    'actual' => $data['action'],
                ]);

                return null;
            }

            return $data['score'] ?? null;
        } catch (\Throwable $e) {
            $this->logger->error('CAPTCHA verification error', [
                'error' => $e->getMessage(),
            ]);

            return null;
        }
    }

    /**
     * Check if CAPTCHA score is acceptable.
     */
    public function isHuman(string $token, string $action, ?string $ip = null): bool
    {
        $score = $this->verify($token, $action, $ip);

        return $score !== null && $score >= self::MIN_SCORE;
    }
}

Audit Logging для событий безопасности

Audit log — неизменяемая запись всех важных событий безопасности. Позволяет расследовать инциденты и обнаруживать аномалии.

Что логировать

Категория События
Аутентификация Login success/failure, logout, password change, MFA enable/disable
Авторизация Permission granted/denied, role change
Данные Create, read, update, delete sensitive data
Конфигурация Settings change, feature toggle, deploy
Безопасность Rate limit triggered, CAPTCHA failure, blocked IP
Административные User create/delete, permission change, API key rotation

Структура audit log записи

{
  "timestamp": "2026-03-01T14:32:15.123Z",
  "event_type": "auth.login.failure",
  "actor": {
    "type": "user",
    "id": "usr_abc123",
    "ip": "192.168.1.100",
    "user_agent": "Mozilla/5.0..."
  },
  "target": {
    "type": "account",
    "id": "usr_abc123"
  },
  "action": "login",
  "outcome": "failure",
  "reason": "invalid_password",
  "metadata": {
    "attempt_number": 3,
    "geo": {"country": "RU", "city": "Moscow"},
    "device_fingerprint": "fp_xyz789"
  }
}

PHP: SecurityAuditLogger

<?php

declare(strict_types=1);

namespace App\Security\Audit;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RequestStack;

final readonly class SecurityAuditLogger
{
    public function __construct(
        private LoggerInterface $auditLogger,  // Monolog channel: "audit"
        private RequestStack $requestStack,
    ) {}

    /**
     * Log an authentication event.
     */
    public function logAuthEvent(
        string $eventType,
        string $outcome,
        ?string $userId = null,
        array $metadata = [],
    ): void {
        $request = $this->requestStack->getCurrentRequest();

        $this->auditLogger->info('Security audit event', [
            'event_type' => $eventType,
            'outcome' => $outcome,
            'actor' => [
                'user_id' => $userId,
                'ip' => $request?->getClientIp(),
                'user_agent' => $request?->headers->get('User-Agent'),
            ],
            'metadata' => $metadata,
            'timestamp' => (new \DateTimeImmutable())->format(\DateTimeInterface::RFC3339_EXTENDED),
        ]);
    }

    /**
     * Log a data access event.
     */
    public function logDataAccess(
        string $action,
        string $resourceType,
        string $resourceId,
        string $userId,
        array $metadata = [],
    ): void {
        $request = $this->requestStack->getCurrentRequest();

        $this->auditLogger->info('Data access audit', [
            'event_type' => "data.{$action}",
            'outcome' => 'success',
            'actor' => [
                'user_id' => $userId,
                'ip' => $request?->getClientIp(),
            ],
            'target' => [
                'type' => $resourceType,
                'id' => $resourceId,
            ],
            'metadata' => $metadata,
            'timestamp' => (new \DateTimeImmutable())->format(\DateTimeInterface::RFC3339_EXTENDED),
        ]);
    }

    /**
     * Log a security violation event.
     */
    public function logSecurityViolation(
        string $violationType,
        string $description,
        ?string $userId = null,
        array $metadata = [],
    ): void {
        $request = $this->requestStack->getCurrentRequest();

        $this->auditLogger->warning('Security violation', [
            'event_type' => "security.{$violationType}",
            'outcome' => 'blocked',
            'actor' => [
                'user_id' => $userId,
                'ip' => $request?->getClientIp(),
                'user_agent' => $request?->headers->get('User-Agent'),
            ],
            'description' => $description,
            'metadata' => $metadata,
            'timestamp' => (new \DateTimeImmutable())->format(\DateTimeInterface::RFC3339_EXTENDED),
        ]);
    }
}

Go: Audit Middleware

package middleware

import (
	"context"
	"encoding/json"
	"log/slog"
	"net/http"
	"time"
)

// AuditEvent represents a security audit log entry.
type AuditEvent struct {
	Timestamp  time.Time         `json:"timestamp"`
	EventType  string            `json:"event_type"`
	Action     string            `json:"action"`
	Outcome    string            `json:"outcome"`
	ActorID    string            `json:"actor_id,omitempty"`
	ActorIP    string            `json:"actor_ip"`
	UserAgent  string            `json:"user_agent"`
	TargetType string            `json:"target_type,omitempty"`
	TargetID   string            `json:"target_id,omitempty"`
	Metadata   map[string]string `json:"metadata,omitempty"`
}

// AuditLogger writes security audit events to a structured log.
type AuditLogger struct {
	logger *slog.Logger
}

// NewAuditLogger creates a new audit logger instance.
func NewAuditLogger(logger *slog.Logger) *AuditLogger {
	return &AuditLogger{
		logger: logger.WithGroup("audit"),
	}
}

// Log writes an audit event.
func (a *AuditLogger) Log(ctx context.Context, event AuditEvent) {
	event.Timestamp = time.Now().UTC()

	data, _ := json.Marshal(event)

	a.logger.InfoContext(ctx, "audit_event",
		slog.String("event_type", event.EventType),
		slog.String("outcome", event.Outcome),
		slog.String("actor_ip", event.ActorIP),
		slog.String("raw", string(data)),
	)
}

// AuditMiddleware logs all incoming HTTP requests for security auditing.
func AuditMiddleware(audit *AuditLogger, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Wrap ResponseWriter to capture status code
		wrapped := &statusWriter{ResponseWriter: w, statusCode: http.StatusOK}

		start := time.Now()
		next.ServeHTTP(wrapped, r)
		duration := time.Since(start)

		outcome := "success"
		if wrapped.statusCode >= 400 {
			outcome = "failure"
		}

		audit.Log(r.Context(), AuditEvent{
			EventType: "http.request",
			Action:    r.Method + " " + r.URL.Path,
			Outcome:   outcome,
			ActorIP:   r.RemoteAddr,
			UserAgent: r.UserAgent(),
			Metadata: map[string]string{
				"status":   http.StatusText(wrapped.statusCode),
				"duration": duration.String(),
				"method":   r.Method,
				"path":     r.URL.Path,
			},
		})
	})
}

type statusWriter struct {
	http.ResponseWriter
	statusCode int
}

func (w *statusWriter) WriteHeader(code int) {
	w.statusCode = code
	w.ResponseWriter.WriteHeader(code)
}

Хранение audit logs

Требование Реализация
Неизменяемость Append-only хранилище, нет операций UPDATE/DELETE
Целостность Хеш-цепочка (каждая запись содержит хеш предыдущей)
Долгосрочность Минимум 1 год хранения, архивация в S3 Glacier
Доступность Поиск по actor, event_type, time range
Защита Отдельные permissions, шифрование at rest
Мониторинг Алерты при аномалиях (массовые failures, admin actions)

Abuse Detection Pipeline

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

                   Входящий запрос
                        │
           ┌────────────▼────────────┐
           │      CloudFlare/CDN     │  L3/L4 DDoS mitigation
           │   IP reputation check   │  Anycast, scrubbing
           └────────────┬────────────┘
                        │
           ┌────────────▼────────────┐
           │         WAF             │  L7 attack filtering
           │   SQL injection, XSS   │  Bot control rules
           │   Managed rule sets     │  Geo-blocking
           └────────────┬────────────┘
                        │
           ┌────────────▼────────────┐
           │    Rate Limiter         │  Per-IP, per-endpoint
           │   (nginx / ALB)        │  Burst protection
           └────────────┬────────────┘
                        │
           ┌────────────▼────────────┐
           │    Bot Detection        │  Honeypot check
           │   (Application)        │  Fingerprint validation
           │                        │  Behavioral analysis
           └────────────┬────────────┘
                        │
           ┌────────────▼────────────┐
           │    App Rate Limiter     │  Per-user limits
           │   Brute force check    │  Progressive delays
           │   CAPTCHA (if needed)  │  Account lockout
           └────────────┬────────────┘
                        │
           ┌────────────▼────────────┐
           │    Application Logic    │  Business rules
           │   Feature gating       │  Authorization
           └────────────┬────────────┘
                        │
           ┌────────────▼────────────┐
           │      Audit Log          │  Who, what, when
           │   Security events      │  Append-only storage
           │   Anomaly detection    │  Alert pipeline
           └─────────────────────────┘

Ключевые принципы пайплайна

Принцип Описание
Defence in depth Каждый слой независим — отказ одного не критичен
Fail closed При сбое WAF — блокируем, а не пропускаем
Graceful degradation Возврат 429 вместо 503 при перегрузке
Observability Каждый слой пишет метрики и логи
Tuning Регулярный анализ false positives / false negatives

Проверь себя

5 из 8
🧪

Почему brute force protection должна комбинировать IP-based и account-based throttling?

🧪

В чём преимущество k-anonymity при проверке паролей через HIBP API?

🧪

Как работает progressive delay при brute force protection?

🧪

Какой уровень DDoS-атаки может быть эффективно остановлен только с помощью WAF?

🧪

Какой метод наиболее эффективен для защиты от credential stuffing?