Belajar Symfony - Rate Limiting & DoS Protection
Episode 20 of 27

Belajar Symfony - Rate Limiting & DoS Protection

Menahan serangan brute-force dan DoS dengan Symfony RateLimiter: konsep limiter (FixedWindow, SlidingWindow, TokenBucket), throttling login per-user & per-IP, membatasi API per klien, caching backend Redis, serta mitigasi di level aplikasi dan infrastruktur.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Episode 19 menutup jalan autentikasi. Episode 20 menutup satu pintu lagi yang sering diabaikan: laju serangan. Brute-force login, spam endpoint, dan DoS (Denial of Service) bekerja dengan satu asumsi: server akan melayani permintaan apa pun sepuasnya. Rate limiting mematahkan asumsi itu.

Mengapa ini penting? Karena pertahanan tanpa rate limiting seperti pintu terkunci tanpa penghitung gagal — tetap bisa dirusak dengan mencoba terus-menerus. Login yang di-rate limit bukan hanya mencegah brute-force, tapi juga melindungi database dari beban query yang tidak perlu.

Konsep RateLimiter

Install component:

Install RateLimiter
composer require symfony/rate-limiter

Limiter diartikan sebagai strategi menghitung konsumsi dalam interval waktu. Tiga strategi bawaan:

StrategiCara kerjaCocok untuk
FixedWindowLimiterReset counter di akhir window tetapSederhana, per menit/jam
SlidingWindowLimiterWindow bergeser mulusBatas yang lebih adil
TokenBucketLimiterToken terisi berkala, burst diizinkanAPI dengan burst

Definisikan di config/packages/rate_limiter.yaml:

config/packages/rate_limiter.yaml
framework:
    rate_limiter:
        anonymous_api:
            policy: 'fixed_window'
            limit: 30
            interval: '1 minute'
 
        authenticated_api:
            policy: 'token_bucket'
            limit: 100
            rate: { interval: '1 minute', amount: 10 }
 
        login_attempts:
            policy: 'sliding_window'
            limit: 5
            interval: '15 minutes'

Tiap limiter punya limit (kuota) dan interval (jendela). Token bucket menambahkan rate — kecepatan pengisian token.

Throttling Login

Serangan brute-force menyasar route login. Proteksi bawaan paling sederhana adalah login_throttling:

Aktifkan login throttling
security:
    firewalls:
        main:
            login_throttling:
                max_attempts: 5
                interval: '15 minutes'

Tanpa menulis kode, 5 percobaan gagal dalam 15 menit memicu penolakan. Namun throttling default membatasi per IP — penyerang yang memakai banyak IP bisa menghindarinya. Kombinasikan dengan per-user limiter:

Rate limit per user & per IP
use Symfony\Component\RateLimiter\RateLimiterFactory;
 
public function login(
    Request $request,
    RateLimiterFactory $loginAttemptsLimiter,
): Response
{
    $identifier = $request->getUser() ?? $request->getClientIp();
    $limiter = $loginAttemptsLimiter->create($identifier);
 
    if (!$limiter->consume()->isAccepted()) {
        throw $this->createTooManyRequestsHttpException(
            60, 'Terlalu banyak percobaan. Coba lagi dalam 1 menit.'
        );
    }
 
    // lanjut proses login...
}

Kombinasi keduanya: IP dibatasi oleh firewall (login_throttling), user dibatasi oleh limiter custom — penyerang yang ganti IP tetap terhadang di limiter per-user.

Rate Limit untuk API

Untuk endpoint API (episode 13), batasi konsumsi per klien. Integrasi paling rapi lewat listener:

src/EventListener/ApiRateLimitListener.php
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\RateLimiter\RateLimiterFactory;
 
#[AsEventListener(event: RequestEvent::class, priority: 40)]
final class ApiRateLimitListener
{
    public function __construct(
        private readonly RateLimiterFactory $anonymousApiLimiter,
        private readonly RateLimiterFactory $authenticatedApiLimiter,
    ) {
    }
 
    public function __invoke(RequestEvent $event): void
    {
        $request = $event->getRequest();
        if (!str_starts_with($request->getPathInfo(), '/api/')) {
            return;
        }
 
        $user = $this->getUser();
        $factory = $user ? $this->authenticatedApiLimiter : $this->anonymousApiLimiter;
        $identifier = $user ? (string) $user->getId() : (string) $request->getClientIp();
 
        if (!$factory->create($identifier)->consume()->isAccepted()) {
            throw new TooManyRequestsHttpException(
                retryAfter: 60,
                message: 'Batas request API tercapai.',
            );
        }
    }
}

Pola penting di sini: identifier berbeda untuk user vs anonymous. User dibatasi per akun; klien yang belum login dibatasi per IP. Lihat juga retryAfter — ini menghasilkan header Retry-After yang informatif.

Backend Cache untuk Limiter

Rate limiter menyimpan state. Default memakai cache.app, tetapi untuk production multi-instance (episode 24), state harus dibagi lintas server — kalau tidak, 5 percobaan di server A + 5 di server B = 10 percobaan lolos. Kunci Redis:

Limiter memakai Redis
framework:
    rate_limiter:
        login_attempts:
            policy: 'sliding_window'
            limit: 5
            interval: '15 minutes'
            cache_pool: cache.rate_limiter
 
        cache:
            pools:
                cache.rate_limiter:
                    adapter: cache.adapter.redis

Dengan ini, semua instance membaca counter yang sama dari Redis — batas benar-benar global.

Warning

Rate limiting di level aplikasi bukan pengganti proteksi level infrastruktur. CDN/WAF (Cloudflare, AWS WAF) dan reverse proxy dengan modul seperti fail2ban tetap dibutuhkan untuk jilatan banjir besar sebelum traffic sampai ke PHP. RateLimiter adalah lapisan kedua — yang membedakan bot cerdas dari ledakan biasa, di sisi aplikasi.

Return Semantik yang Benar

Saat limiter menolak, balas dengan kode dan header yang sesuai HTTP:

Response 429 yang benar
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1723802400

Kode 429 memberi tahu klien (dan WAF di depannya) bahwa request tertahan sementara — bukan kegagalan permanen. Header Retry-After memberi instruksi kapan boleh mencoba lagi.

Common Pitfalls

  • Limiter memakai cache default di multi-instance — counter terpecah; selalu tunjuk ke Redis pool shared.
  • Identifier yang terlalu lebar — rate limit per IP untuk endpoint yang dipakai banyak user di belakang NAT (kantor) akan memblokir semua; pilih identifier sesuai konteks.
  • Memblokir semua request di test — batas limiter ikut dipakai test; set limiter berbeda (atau cache_pool memory) di env test.
  • Lupa memanggil reset() — tidak ada reset otomatis saat window berubah; konsumsi dihitung per key di window aktif.

Penutup

Pada episode 20 ini, kalian telah menahan laju serangan.

Inti yang harus dibawa pulang:

  • Tiga strategi limiter: FixedWindow, SlidingWindow, TokenBucket.
  • login_throttling bawaan untuk brute-force login per IP.
  • Limiter per-user & per-IP via listener; 429 + Retry-After yang benar.
  • Redis backend untuk counter yang konsisten lintas instance.
  • Rate limiter aplikasi melengkapi proteksi WAF/CDN, bukan menggantikannya.

Di episode 21 selanjutnya kita kembali ke database untuk skala besar: Advanced Doctrine — QueryBuilder & DQL, hydration modes, pagination besar, dan menyelesaikan masalah N+1 dengan fetch join. Sampai jumpa di episode 21!

Belajar Symfony - Rate Limiting & DoS Protection | Belajar Symfony