Skip to content

Rate Limit Saja Tidak Cukup: Dummy Hash untuk Login

Adityo Guni Waluyo

Dua lapis pertahanan endpoint login: sliding window per IP dan per akun, plus ekualisasi waktu dengan dummy hash bcrypt.

Ringkasan

Endpoint login DemandScope tadinya gampang dibobol karena tanpa batas percobaan dan responsnya lebih cepat buat email yang nggak terdaftar. Solusinya dua lapis: rate limit berbasis sliding window pakai IP asli, plus hashing bcrypt dummy biar waktu respons nggak membocorkan keberadaan akun. Bonusnya, /docs ditutup di produksi dan duplikat email kini balas HTTP 409.

Uji coba asap di lingkungan produksi menunjukkan hasil yang konsisten. Pada percobaan login ke-11 dalam rentang satu menit, server menjawab dengan status HTTP 429 disertai header Retry-After. Respons ini bukan kebetulan, melainkan hasil dari proses pengerasan keamanan endpoint autentikasi pada sebuah layanan FastAPI fiktif bernama DemandScope.

Sebelum perubahan ini diterapkan, endpoint login punya dua celah. Sistem mengizinkan percobaan login tanpa batas, dan merespons lebih cepat untuk alamat surel yang tidak terdaftar karena langsung kembali sebelum menjalankan proses hashing kata sandi. OWASP menyebut pola ini sebagai quick exit [1]. Efeknya, kecepatan respons menjadi penanda keberadaan akun.

Awalnya saya menduga rate limit saja sudah cukup menutup celah enumerasi pengguna tersebut. Dugaan itu keliru. Pembatasan permintaan memperlambat serangan brute force, tetapi tidak menghapus perbedaan waktu respons yang menjadi indikator keberadaan akun.

Dua lapis, dua tugas

Yang ternyata diperlukan adalah dua mekanisme dengan tugas yang tidak saling menggantikan. Lapisan pertama adalah pembatas laju berbasis sliding window dengan dual keying. Konfigurasinya: 30 permintaan per menit per alamat IP untuk login, 10 permintaan per menit per akun, dan 20 permintaan per menit per IP untuk pendaftaran. Kunci pembatas selalu memakai alamat IP peer langsung dan tidak pernah mempercayai header X-Forwarded-For, sehingga pengirim tidak bisa memalsukan identitas jaringannya untuk menghindari pembatasan.

Lapisan kedua adalah ekualisasi waktu. Ketika alamat surel tidak ditemukan di basis data, sistem tetap menjalankan bcrypt terhadap sebuah dummy hash yang konstan [1]. Work factor hash dummy harus sama dengan hash asli, supaya biaya komputasi dan latensi respons identik baik ketika akun ada maupun tidak. OWASP merekomendasikan work factor minimal 10 untuk bcrypt [4].

Pola ini berdiri di atas standar. RFC 6585 mendefinisikan 429 sebagai kondisi terlalu banyak permintaan dalam rentang waktu tertentu, dan menyatakan respons boleh menyertakan Retry-After [2]. RFC 9110 melanjutkan: nilai header itu berupa HTTP-date atau bilangan bulat delay-seconds [3]. Bagi klien, angka detik pada header itulah yang dipakai untuk menunggu sebelum mencoba lagi.

Implementasi minimum yang bisa diuji

Pola verifikasi dengan hash dummy bisa ditulis sesederhana berikut:

import bcrypt

# Precomputed once, same work factor as real password hashes
DUMMY_BCRYPT_HASH = b"$2b$12$C6UzMDM.H6dfI/f/IKcEeO7ZBpUvXPZjWFFMXqFQqVH2tXEuhtbCS"

def verify_login(email: str, password: str) -> bool:
    user = database.get_user_by_email(email)
    if user:
        return bcrypt.checkpw(password.encode("utf-8"), user.password_hash)
    # Run the same expensive check so latency does not reveal existence
    bcrypt.checkpw(password.encode("utf-8"), DUMMY_BCRYPT_HASH)
    return False

Pembatas laju versi minimum memakai kamus berisi deque per kunci:

import time
from collections import deque

# State lives per process; fine for a single-container deployment
request_history = {}

def is_allowed(key: str, limit: int, window_seconds: int) -> bool:
    now = time.time()
    history = request_history.setdefault(key, deque())
    while history and history[0] <= now - window_seconds:
        history.popleft()
    if len(history) >= limit:
        return False
    history.append(now)
    return True

Perlu diakui terus terang: state di kode itu hidup per proses. Untuk deployment satu kontainer seperti DemandScope saat ini, itu cukup. Bila jumlah worker dinaikkan, hitungan tiap proses tidak berbagi, dan batas efektif bisa melonggar; itu pertukaran yang disadari, bukan sesuatu yang disembunyikan.

Permukaan API ikut ditutup

Perlindungan tidak berhenti di endpoint login. Secara bawaan FastAPI menyajikan dokumentasi interaktif di jalur /docs [5]. Pada DemandScope, jalur itu beserta skema OpenAPI kini hanya disajikan ketika variabel lingkungan APP_ENV bernilai dev, sehingga peta endpoint tidak dipublikasikan di produksi. Sebagai bonus, kondisi race pendaftaran dengan surel duplikat kini dipetakan dari error integritas basis data menjadi HTTP 409, sehingga klien menerima pesan yang aman tanpa membocorkan detail skema.

Pemisahan tugas antara pembatasan laju dan penyamaran waktu respons membuat permukaan serangan menyempit dari dua arah. Sistem tidak lagi memberi petunjuk lewat kecepatan respons, sementara volume serangan kasar teredam sebelum membebani sumber daya komputasi.

Sumber

[1] OWASP Authentication Cheat Sheet, cheatsheetseries.owasp.org

[2] RFC 6585, Additional HTTP Status Codes, rfc-editor.org

[3] RFC 9110, HTTP Semantics bagian 10.2.3, rfc-editor.org

[4] OWASP Password Storage Cheat Sheet, cheatsheetseries.owasp.org

[5] FastAPI class reference docs_url, fastapi.tiangolo.com

Artikel terkait