Brute Force #

Brute force adalah serangan yang mencoba semua kemungkinan kombinasi sampai menemukan yang benar. Dalam konteks web security, ini paling sering berarti mencoba ribuan atau jutaan kombinasi password untuk satu akun, atau mencoba daftar credential yang sudah bocor dari pelanggaran data sebelumnya. Tidak ada kecanggihan teknis yang diperlukan — hanya automasi dan kesabaran.

Yang membuat brute force relevan dan berbahaya di 2025: jumlah credential yang bocor dari data breach sudah mencapai miliaran pasang. Koleksi seperti RockYou2021 mengandung 8.4 miliar password unik. Attacker tidak perlu menebak dari nol — mereka menggunakan credential yang sudah bocor dari satu layanan untuk mencoba masuk ke layanan lain (credential stuffing), asumsinya banyak orang menggunakan password yang sama di berbagai platform.

Sistem yang tidak memiliki perlindungan brute force adalah sistem yang menunggu dieksploitasi — bukan apakah, tapi kapan.

Empat Jenis Serangan Brute Force #

graph TD
    A[Brute Force Attacks] --> B[Credential Stuffing]
    A --> C[Password Spraying]
    A --> D[Dictionary Attack]
    A --> E[Pure Brute Force]

    B --> B1["Gunakan credential bocor\ndari breach lain\n1 password per target"]
    C --> C1["1 password umum\nke banyak akun\nHindari lockout"]
    D --> D1["Wordlist password umum\nTop 10M passwords\nMangling rules"]
    E --> E1["Semua kombinasi karakter\na, b, c... aa, ab...\nHanya untuk password pendek"]

    B1 --> F["Skala besar, akurasi tinggi\nSuccess rate 0.1-2%"]
    C1 --> G["Tidak trigger lockout\nLambat tapi persistent"]
    D1 --> H[Efektif untuk password lemah]
    E1 --> I[Tidak praktis untuk 12+ karakter]

Credential Stuffing #

Ini adalah ancaman paling serius saat ini. Attacker mengambil pasangan email:password dari breach yang sudah ada dan mencobanya secara otomatis ke ratusan layanan berbeda.

Mengapa credential stuffing sangat efektif:

  Data yang tersedia:
  → RockYou2021: 8.4 miliar credential
  → Collection #1-5: 2.2 miliar unik credential
  → Breach dari LinkedIn, Adobe, Yahoo, dll masih aktif digunakan

  Success rate:
  → 0.1% - 2% credential masih valid di layanan lain
  → Dengan 10 juta credential: 10.000 - 200.000 akun berhasil dikompromikan

  Alat yang digunakan:
  → Sentry MBA, OpenBullet, Snipr — tool otomatis yang tersedia bebas
  → Konfigurasi per-site, mendukung proxy rotation, CAPTCHA bypass
  → Bisa berjalan dari ribuan IP berbeda secara bersamaan

  Tanda-tanda credential stuffing:
  → Login failure rate tiba-tiba naik
  → Login berhasil dari IP/lokasi yang tidak biasa
  → Banyak "successful login" tapi user tidak mengenalinya
  → Traffic login dari berbagai IP yang berbeda

Password Spraying #

Alih-alih mencoba banyak password untuk satu akun (yang akan trigger lockout), password spraying mencoba satu atau beberapa password yang sangat umum ke banyak akun. Teknik ini khususnya efektif di lingkungan enterprise.

Password spraying vs traditional brute force:

  Traditional brute force (mudah terdeteksi):
  Account: [email protected]
  Attempt 1: password123
  Attempt 2: qwerty123
  Attempt 3: ali123
  ... (ribuan attempt → trigger lockout setelah 5 attempt)

  Password spraying (sulit terdeteksi):
  Password: Spring2025!  (memenuhi policy tapi mudah ditebak)

  Attempt 1: [email protected] → Spring2025!    (tidak lock)
  Attempt 2: [email protected] → Spring2025!   (tidak lock)
  Attempt 3: [email protected] → Spring2025!  (tidak lock)
  ...ribuan akun...
  Attempt N: [email protected] → Spring2025!  ← BERHASIL

  Setiap akun hanya dicoba sekali atau dua kali → tidak trigger lockout
  Attacker menunggu beberapa jam antara batch untuk lebih aman

Rate Limiting yang Efektif #

Rate limiting adalah pertahanan pertama terhadap semua bentuk brute force. Tapi implementasi yang naif bisa dibypass dengan mudah.

// Rate limiter berlapis untuk endpoint login:
// 1. Per IP — melindungi dari satu IP yang agresif
// 2. Per akun — melindungi dari credential stuffing dari banyak IP
// 3. Per IP + akun — kombinasi yang paling presisi

// Konfigurasi: (maxAttempts, windowSeconds, lockoutSeconds)
var (
	ipLimit      = [3]int{30, 300, 300}    // 30 per 5 menit, lockout 5 menit
	accountLimit = [3]int{5, 900, 900}     // 5 per 15 menit, lockout 15 menit
	globalLimit  = [3]int{10000, 60, -1}   // Global: 10k per menit (DDoS guard)
)

type RateLimitResult struct {
	Allowed           bool
	RemainingAttempts int
	ResetAfterSeconds int
	LockoutSeconds    int // -1 jika tidak ada lockout
}

type LoginRateLimiter struct{}

// CheckAll mengecek semua rate limit dan mencatat attempt.
// Return hasil dengan Allowed=false jika salah satu limit terlampaui.
func (l LoginRateLimiter) CheckAll(ip, email string) RateLimitResult {
	// Cek lockout aktif
	if result, locked := l.checkLockout(ip, email); locked {
		return result
	}

	// Cek rate limit per IP
	ipResult := l.checkLimit("rl:ip:"+ip, ipLimit)
	if !ipResult.Allowed {
		l.setLockout("lockout:ip:"+ip, ipLimit[2])
		return ipResult
	}

	// Cek rate limit per akun (email di-hash untuk privacy)
	emailHash := hashEmail(email)
	accountResult := l.checkLimit("rl:account:"+emailHash, accountLimit)
	if !accountResult.Allowed {
		l.setLockout("lockout:account:"+emailHash, accountLimit[2])
		return accountResult
	}

	remaining := ipResult.RemainingAttempts
	if accountResult.RemainingAttempts < remaining {
		remaining = accountResult.RemainingAttempts
	}
	reset := ipResult.ResetAfterSeconds
	if accountResult.ResetAfterSeconds < reset {
		reset = accountResult.ResetAfterSeconds
	}
	return RateLimitResult{Allowed: true, RemainingAttempts: remaining, ResetAfterSeconds: reset}
}

func (l LoginRateLimiter) checkLimit(key string, limit [3]int) RateLimitResult {
	now := time.Now().Unix()
	// Redis sorted set: hapus entri lama, hitung, tambah, set TTL
	redis.ZRemRangeByScore(key, "0", strconv.FormatInt(now-int64(limit[1]), 10))
	count := redis.ZCard(key)
	redis.ZAdd(key, redis.Z{Score: float64(now), Member: strconv.FormatInt(now, 10)})
	redis.Expire(key, time.Duration(limit[1])*time.Second)

	remaining := limit[0] - int(count) - 1
	if remaining < 0 {
		remaining = 0
	}
	return RateLimitResult{
		Allowed:           count < int64(limit[0]),
		RemainingAttempts: remaining,
		ResetAfterSeconds: limit[1],
	}
}

func (l LoginRateLimiter) checkLockout(ip, email string) (RateLimitResult, bool) {
	for _, key := range []string{"lockout:ip:" + ip, "lockout:account:" + hashEmail(email)} {
		if ttl := redis.TTL(key).Seconds(); ttl > 0 {
			return RateLimitResult{
				Allowed:           false,
				RemainingAttempts: 0,
				ResetAfterSeconds: int(ttl),
				LockoutSeconds:    int(ttl),
			}, true
		}
	}
	return RateLimitResult{}, false
}

func (l LoginRateLimiter) setLockout(key string, seconds int) {
	redis.SetEX(key, time.Duration(seconds)*time.Second, 1)
}

// ResetOnSuccess membersihkan counter setelah login berhasil.
func (l LoginRateLimiter) ResetOnSuccess(ip, email string) {
	redis.Del("rl:ip:"+ip, "rl:account:"+hashEmail(email),
		"lockout:ip:"+ip, "lockout:account:"+hashEmail(email))
}

func hashEmail(email string) string {
	sum := sha256.Sum256([]byte(strings.ToLower(email)))
	return hex.EncodeToString(sum[:])[:16]
}

func main() {
	var loginLimiter LoginRateLimiter

	// Registrasi router (pseudo-code, meniru route Flask)
	app.Post("/login", func(req Request, res Response) {
		ip := getRealIP(req) // handle proxy headers dengan hati-hati
		email := strings.ToLower(strings.TrimSpace(req.JSON("email")))

		// Cek rate limit sebelum apapun
		result := loginLimiter.CheckAll(ip, email)
		if !result.Allowed {
			retryAfter := result.ResetAfterSeconds
			if result.LockoutSeconds > 0 {
				retryAfter = result.LockoutSeconds
			}
			res.Status(429)
			res.Header("Retry-After", strconv.Itoa(retryAfter))
			res.JSON(map[string]any{"error": "Too many attempts", "retry_after": retryAfter})
			return
		}

		// Proses login dengan timing yang konsisten
		user := authenticateUser(email, req.JSON("password"))
		if user != nil {
			loginLimiter.ResetOnSuccess(ip, email)
			setAuthCookie(res, createSession(user.ID, req))
			res.JSON(map[string]any{"status": "ok"})
			return
		}

		// Login gagal — pesan yang sama untuk semua kasus
		res.Status(401)
		res.JSON(map[string]any{"error": "Email atau password tidak valid"})
	})
}

Timing Attack: Celah yang Sering Diabaikan #

Waktu yang dibutuhkan untuk memproses login request bisa membocorkan informasi kepada attacker.

// ANTI-PATTERN: timing leak — attacker bisa bedakan "email tidak ada" vs "password salah"
func main() {
	app.Post("/login", func(req Request, res Response) {
		email := req.JSON("email")
		password := req.JSON("password")

		user := findUserByEmail(email)

		if user == nil {
			// Return CEPAT — tidak ada hash computation
			res.Status(401)
			res.JSON(map[string]any{"error": "Invalid credentials"})
			return
		}

		if !verifyPassword(password, user.PasswordHash) {
			// Return LAMBAT — hash computation terjadi
			res.Status(401)
			res.JSON(map[string]any{"error": "Invalid credentials"})
			return
		}
	})
}

// Attacker mengukur response time:
// < 5ms = email tidak ada di database (langsung return)
// > 100ms = email ada tapi password salah (hash computation terjadi)
// → Attacker bisa enumerate email valid tanpa trigger rate limit

// BENAR: waktu respons yang konsisten
var ph = PasswordHasher{}

// Hash dummy yang valid untuk digunakan ketika user tidak ditemukan
var dummyHash = ph.Hash("dummy_password_that_never_matches")

func init() {
	app.Post("/login", func(req Request, res Response) {
		email := req.JSON("email")
		password := req.JSON("password")

		user := findUserByEmail(email)
		passwordHash := dummyHash
		if user != nil {
			passwordHash = user.PasswordHash
		}

		// Verifikasi selalu dijalankan, terlepas apakah user ada atau tidak
		isValid, err := ph.Verify(passwordHash, password)
		if err == nil && user != nil && isValid {
			// Login berhasil
			res.JSON(successResponse(user))
			return
		}

		// Login gagal — pesan yang sama
		res.Status(401)
		res.JSON(map[string]any{"error": "Email atau password tidak valid"})
	})
}

Account Lockout yang Tidak Merugikan User Legitimate #

Account lockout yang terlalu agresif bisa digunakan oleh attacker sebagai serangan availability: mereka sengaja mengunci akun user legitimate dengan mencoba password yang salah.

// Lockout yang cerdas:
// - Tidak langsung lockout permanen
// - Progressive delay
// - Tidak memungkinkan attacker untuk DOS akun user lain

type SmartAccountLockout struct {
	// Progressive lockout: semakin banyak gagal, semakin lama locked
	lockoutSchedule [][2]int
}

func NewSmartAccountLockout() *SmartAccountLockout {
	return &SmartAccountLockout{
		lockoutSchedule: [][2]int{
			{3, 30},     // Setelah 3 gagal: lock 30 detik
			{5, 300},    // Setelah 5 gagal: lock 5 menit
			{10, 1800},  // Setelah 10 gagal: lock 30 menit
			{20, 86400}, // Setelah 20 gagal: lock 24 jam
		},
	}
}

// GetLockoutDuration mengembalikan durasi lockout berdasarkan jumlah kegagalan.
func (s *SmartAccountLockout) GetLockoutDuration(failureCount int) int {
	duration := 0
	for _, entry := range s.lockoutSchedule {
		if failureCount >= entry[0] {
			duration = entry[1]
		}
	}
	return duration
}

func (s *SmartAccountLockout) RecordFailure(accountKey string) map[string]any {
	key := "login_failures:" + accountKey

	// Increment failure count
	failureCount := redis.Incr(key)

	// Set expiry jika baru dibuat
	if failureCount == 1 {
		redis.Expire(key, 24*time.Hour) // reset setelah 24 jam tanpa activity
	}

	// Tentukan lockout duration
	lockoutDuration := s.GetLockoutDuration(failureCount)
	if lockoutDuration > 0 {
		redis.SetEX("lockout:"+accountKey, time.Duration(lockoutDuration)*time.Second, failureCount)
	}

	return map[string]any{
		"failure_count":   failureCount,
		"locked":          lockoutDuration > 0,
		"lockout_seconds": lockoutDuration,
	}
}

// IsLocked mengembalikan (is_locked, seconds_remaining).
func (s *SmartAccountLockout) IsLocked(accountKey string) (bool, int) {
	ttl := int(redis.TTL("lockout:" + accountKey).Seconds())
	if ttl < 0 {
		ttl = 0
	}
	return ttl > 0, ttl
}

// UnlockOnSuccess reset setelah login berhasil.
func (s *SmartAccountLockout) UnlockOnSuccess(accountKey string) {
	redis.Del("login_failures:"+accountKey, "lockout:"+accountKey)
}
Strategi lockout yang aman:

  ✓ Progressive lockout — semakin banyak gagal, semakin lama locked
    Attacker yang mencoba 100x akan kena lockout lama
    User yang lupa password biasanya mencoba 2-3x

  ✓ Soft lockout dengan notifikasi email
    "Akun Anda sementara terkunci. Jika bukan Anda yang mencoba login,
    klik di sini untuk mengamankan akun."

  ✓ Unlock via email verification
    Kirim link unlock ke email terdaftar
    Attacker tidak bisa unlock kecuali punya akses ke email

  ✗ Jangan lockout permanent tanpa mekanisme unlock
    User yang legitimate tidak bisa masuk ke akunnya sendiri

  ✗ Jangan lockout berdasarkan IP saja
    Satu IP bisa share banyak user (NAT, corporate network)
    Lockout IP bisa kena user innocent

CAPTCHA: Efektif tapi Bukan Solusi Lengkap #

CAPTCHA mempersulit automasi dengan menghadirkan challenge yang mudah bagi manusia tapi sulit bagi mesin.

// Integrasi Google reCAPTCHA v3 (invisible, berbasis score)
var (
func main() {
		recaptchaSecret    = os.Getenv("RECAPTCHA_SECRET_KEY")
}
func init() {
		recaptchaVerifyURL = "https://www.google.com/recaptcha/api/siteverify"
}
func init() {
		recaptchaThreshold = 0.5 // Score 0 (bot) sampai 1 (manusia)
}
)

// VerifyRecaptcha memverifikasi reCAPTCHA token.
// Returns: (is_human, score)
func VerifyRecaptcha(token, action string) (bool, float64) {
	resp, err := http.PostForm(recaptchaVerifyURL, url.Values{
		"secret":   []string{recaptchaSecret},
		"response": []string{token},
	})
	if err != nil {
		// Jika CAPTCHA service down, jangan blokir user legitimate
		// Tapi log untuk monitoring
func init() {
			logger.Warning("reCAPTCHA verification failed — service unavailable")
}
		return true, 1.0 // Default allow jika service down
	}
	defer resp.Body.Close()

	var result struct {
		Success bool    `json:"success"`
		Action  string  `json:"action"`
		Score   float64 `json:"score"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return false, 0.0
	}

	if !result.Success {
		return false, 0.0
	}

	// Verifikasi action sesuai
	if result.Action != action {
		return false, 0.0
	}

	return result.Score >= recaptchaThreshold, result.Score
}

// Registrasi router (pseudo-code, meniru route Flask)
func init() {
	app.Post("/login", func(req Request, res Response) {
		// Ambil CAPTCHA token dari request
		captchaToken := req.JSON("captcha_token")
		if captchaToken == "" {
			res.Status(400)
			res.JSON(map[string]any{"error": "CAPTCHA required"})
			return
		}

		isHuman, score := VerifyRecaptcha(captchaToken, "login")
		if !isHuman {
			logger.Warning("Low reCAPTCHA score on login",
				"score", score, "ip", req.RemoteAddr)
			res.Status(429)
			res.JSON(map[string]any{"error": "CAPTCHA verification failed"})
			return
		}

		// Lanjutkan proses login
		// ...
	})
}
Keterbatasan CAPTCHA:

  reCAPTCHA v2 (teka-teki gambar):
  → Bisa diselesaikan oleh manusia yang dibayar murah (CAPTCHA farm)
  → Harga: $1 per 1000 solves — attacker bisa bypass dengan murah

  reCAPTCHA v3 (invisible, score-based):
  → Tidak mengganggu UX
  → Bisa di-bypass dengan menggunakan browser nyata
  → Masih efektif untuk mendeteksi bot sederhana

  hCaptcha, Turnstile (Cloudflare):
  → Alternatif yang makin populer
  → Trade-off privacy berbeda dari Google

  CAPTCHA bukan solusi standalone:
  → Gunakan bersama rate limiting dan account lockout
  → CAPTCHA melengkapi, bukan menggantikan

Deteksi Credential Stuffing #

Pola traffic credential stuffing berbeda dari login normal — bisa dideteksi dengan analisis yang tepat.

// GeoIP database untuk deteksi lokasi
var reader = geoip2.Open("/path/to/GeoLite2-City.mmdb")

type CredentialStuffingDetector struct {
	Window int
}

// NewCredentialStuffingDetector membuat detector dengan window tertentu.
func NewCredentialStuffingDetector(windowSeconds int) *CredentialStuffingDetector {
	return &CredentialStuffingDetector{Window: windowSeconds}
}

// AnalyzeLoginPattern menganalisis apakah request ini terlihat seperti credential stuffing.
func (d *CredentialStuffingDetector) AnalyzeLoginPattern(ip, email string) map[string]any {
	signals := []string{}

	// Signal 1: User-Agent yang tidak umum atau sama persis antar request
	userAgent := req.UserAgent()
	if d.isSuspiciousUserAgent(userAgent) {
		signals = append(signals, "suspicious_user_agent")
	}

	// Signal 2: Tidak ada header yang biasanya ada di browser nyata
	expectedHeaders := []string{"accept", "accept-language", "accept-encoding"}
	missing := 0
	for _, h := range expectedHeaders {
		if req.Header(h) == "" {
			missing++
		}
	}
	if missing > 1 {
		signals = append(signals, "missing_browser_headers")
	}

	// Signal 3: Login dari banyak email berbeda dari satu IP dalam waktu singkat
	ipKey := "logins_from_ip:" + ip
	emailsFromIP := redis.PFCount(ipKey) // HyperLogLog untuk counting
	if emailsFromIP > 10 {
		signals = append(signals, "many_accounts_from_ip")
	}

	// Signal 4: Lokasi yang tidak konsisten dengan pola login historis
	country, err := reader.Country(ip)
	if err == nil && d.isImpossibleLocation(email, country) {
		signals = append(signals, "impossible_location")
	}

	// Signal 5: IP address dari daftar proxy/VPN/Tor yang diketahui
	if d.isKnownProxyIP(ip) {
		signals = append(signals, "proxy_ip")
	}

	// Record email ini dari IP (dengan TTL)
	redis.PFAdd(ipKey, email)
	redis.Expire(ipKey, time.Duration(d.Window)*time.Second)

	riskScore := len(signals)
	return map[string]any{
		"risk_score": riskScore,
		"signals":    signals,
		"action":     d.determineAction(riskScore),
	}
}

func (d *CredentialStuffingDetector) isSuspiciousUserAgent(ua string) bool {
	// Tool credential stuffing sering menggunakan UA yang aneh
	suspiciousPatterns := []string{
		"python-requests", "axios/", "okhttp/", "curl/",
		"go-http-client", "java/", "php/",
	}
	uaLower := strings.ToLower(ua)
	for _, p := range suspiciousPatterns {
		if strings.Contains(uaLower, p) {
			return true
		}
	}
	return false
}

func (d *CredentialStuffingDetector) determineAction(riskScore int) string {
	switch {
	case riskScore == 0:
		return "allow"
	case riskScore == 1:
		return "monitor" // log tapi izinkan
	case riskScore == 2:
		return "challenge" // tampilkan CAPTCHA
	default:
		return "block" // blokir sementara
	}
}

func (d *CredentialStuffingDetector) isKnownProxyIP(ip string) bool {
	// Cek terhadap database IP proxy/VPN/Tor yang diketahui
	// Bisa menggunakan layanan seperti IPQualityScore, MaxMind, dll
	proxyKey := "known_proxy:" + ip
	return redis.Exists(proxyKey) == 1
}

func (d *CredentialStuffingDetector) isImpossibleLocation(email, currentCountry string) bool {
	// Bandingkan dengan country terakhir yang diketahui
	historyKey := "login_country:" + hashEmail(email)
	lastCountry := redis.Get(historyKey)
	return lastCountry != "" && lastCountry != currentCountry
}

// Registrasi router (pseudo-code, meniru route Flask)
func main() {
	detector := NewCredentialStuffingDetector(300)

	app.Post("/login", func(req Request, res Response) {
		ip := req.RemoteAddr
		email := req.JSON("email")

		// Analisis credential stuffing signals
		analysis := detector.AnalyzeLoginPattern(ip, email)

		if analysis["action"] == "block" {
			res.Status(429)
			res.JSON(map[string]any{"error": "Aktivitas mencurigakan terdeteksi", "retry_after": 300})
			return
		}

		if analysis["action"] == "challenge" {
			// Minta CAPTCHA untuk request mencurigakan
			res.Status(202)
			res.JSON(map[string]any{"challenge_required": true, "challenge_type": "captcha"})
			return
		}

		// Lanjutkan proses login normal
		// ...
	})
}

Breached Password Detection #

Menolak password yang sudah diketahui bocor adalah langkah proaktif yang sangat efektif.

const hibpAPIURL = "https://api.pwnedpasswords.com/range/{}"

// CheckPasswordBreached mengecek apakah password ada dalam database HaveIBeenPwned.
// Menggunakan k-Anonymity — password tidak dikirim ke API.
// Returns: (is_breached, breach_count)
func CheckPasswordBreached(password string) (bool, int) {
	sum := sha1.Sum([]byte(password))
	sha1Hex := strings.ToUpper(hex.EncodeToString(sum[:]))
	prefix, suffix := sha1Hex[:5], sha1Hex[5:]

	resp, err := http.Get(fmt.Sprintf(hibpAPIURL, prefix))
	if err != nil {
		// Jika API tidak tersedia, jangan blokir — log dan lanjutkan
		logger.Warning("HIBP API unavailable")
		return false, 0
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return false, 0
	}

	for _, line := range strings.Split(string(body), "\n") {
		parts := strings.Split(line, ":")
		if len(parts) == 2 && parts[0] == suffix {
			count, _ := strconv.Atoi(parts[1])
			return true, count
		}
	}
	return false, 0
}

// Gunakan saat registrasi dan saat ganti password
func ValidatePasswordStrength(password string) []string {
	var errors []string

	if len(password) < 12 {
		errors = append(errors, "Password minimal 12 karakter")
	}

	isBreached, count := CheckPasswordBreached(password)
	if isBreached {
		errors = append(errors,
			fmt.Sprintf("Password ini ditemukan dalam %d data breach. Gunakan password yang unik dan belum pernah digunakan.", count))
	}

	return errors
}

Monitoring dan Alerting #

Brute force yang tidak terdeteksi bisa berjalan berhari-hari tanpa ketahuan.

// Alert metric yang harus dipantau

// 1. Login failure rate — naik signifikan = tanda serangan
// Baseline: X failure per menit di jam normal
// Alert: jika melebihi 5x baseline selama 5 menit berturut

// 2. Unique email count dari satu IP dalam 5 menit
// Baseline: 1-2 email per IP
// Alert: > 10 email berbeda dari satu IP

// 3. Login success rate — turun signifikan = tanda serangan
// Baseline: 70-80% request login berhasil
// Alert: jika success rate turun di bawah 30% selama 10 menit

// 4. Akun yang sukses login dari lokasi baru
// Alert: user yang biasanya login dari Jakarta
//        tiba-tiba login dari IP di Eropa Timur

// Implementasi sederhana dengan counter di Redis
func RecordLoginMetric(success bool, ip, email string) {
	now := time.Now().Unix()
	minuteBucket := now - (now % 60)

	pipe := redis.Pipeline()
	if success {
		pipe.Incr(fmt.Sprintf("login:success:%d", minuteBucket))
	} else {
		pipe.Incr(fmt.Sprintf("login:failure:%d", minuteBucket))
		// Track unique emails per IP
		pipe.SAdd(fmt.Sprintf("login:ips:%s:%d", ip, minuteBucket), email)
		pipe.Expire(fmt.Sprintf("login:ips:%s:%d", ip, minuteBucket), 600*time.Second)
	}

	pipe.Expire(fmt.Sprintf("login:success:%d", minuteBucket), 600*time.Second)
	pipe.Expire(fmt.Sprintf("login:failure:%d", minuteBucket), 600*time.Second)
	pipe.Execute()
}

func CheckBruteForceAlert() []string {
	now := time.Now().Unix()
	var alerts []string

	// Cek 5 menit terakhir
	totalSuccess, totalFailure := 0, 0
	for i := 0; i < 5; i++ {
		bucket := now - (now % 60) - int64(i*60)
		success := redis.GetInt(fmt.Sprintf("login:success:%d", bucket))
		failure := redis.GetInt(fmt.Sprintf("login:failure:%d", bucket))
		totalSuccess += success
		totalFailure += failure
	}

	total := totalSuccess + totalFailure
	if total > 0 {
		failureRate := float64(totalFailure) / float64(total)
		if failureRate > 0.7 && total > 100 {
			alerts = append(alerts, fmt.Sprintf(
				"High login failure rate: %.1f%% (%d/%d failed in last 5 min)",
				failureRate*100, totalFailure, total))
		}
	}

	return alerts
}

Anti-Pattern yang Harus Dihindari #

// ✗ Anti-pattern 1: tidak ada rate limiting pada login endpoint
func loginNoRateLimit(req Request, res Response) {
	user := authenticate(req.JSON("email"), req.JSON("password"))
	_ = user
	// Tidak ada rate limit — bisa coba jutaan password per detik
}

// ✗ Anti-pattern 2: pesan error yang membedakan kasus
func leakyLogin(req Request, res Response) {
	if !userExists {
		res.Status(401)
		res.Write("Email tidak terdaftar") // bocor: email tidak ada
	}
	if wrongPassword {
		res.Status(401)
		res.Write("Password salah") // bocor: email valid
	}
	// ✓ Solusi: "Email atau password tidak valid" untuk semua kasus
}

// ✗ Anti-pattern 3: lockout yang bisa di-abuse untuk DOS akun orang lain
// Attacker sengaja mencoba 10x dengan email target
// → Akun target terkunci
// ✓ Solusi: progressive delay + notifikasi email, bukan hard lockout permanen

// ✗ Anti-pattern 4: rate limit hanya per IP
// Attacker menggunakan ribuan IP berbeda (botnet, residential proxy)
// → Setiap IP hanya 1-2 attempt → tidak pernah trigger IP limit
// ✓ Solusi: rate limit per akun JUGA, bukan hanya per IP

// ✗ Anti-pattern 5: tidak ada monitoring
// Credential stuffing berjalan berhari-hari tanpa terdeteksi
// ✓ Solusi: alert untuk anomali login failure rate

// ✗ Anti-pattern 6: reset rate limit setelah successful login dari IP yang sama
// Attacker berhasil login ke satu akun → rate limit direset untuk IP itu
// → Bisa lanjut coba akun lain dari IP yang sama tanpa limit
// ✓ Solusi: reset hanya untuk akun yang berhasil, bukan untuk IP

Checklist Brute Force Protection #

RATE LIMITING:
  □ Rate limit per IP (lebih longgar — bisa shared NAT)
  □ Rate limit per akun/email (lebih ketat)
  □ Rate limit global endpoint login (guard terhadap DDoS)
  □ Progressive: semakin banyak gagal, semakin lama wait
  □ Retry-After header dikirim di response 429

ACCOUNT LOCKOUT:
  □ Progressive lockout (bukan langsung permanent)
  □ Notifikasi email saat akun terkunci
  □ Mekanisme unlock via email (bukan hanya tunggu timeout)
  □ Attacker tidak bisa lockout akun orang lain secara trivial

TIMING & INFORMATION:
  □ Waktu respons konsisten: email tidak ada ≈ password salah
  □ Pesan error tidak membedakan "email tidak ada" vs "password salah"
  □ Dummy hash computation saat email tidak ditemukan

CREDENTIAL STUFFING:
  □ HIBP API check pada registrasi dan ganti password
  □ Deteksi User-Agent yang mencurigakan (python-requests, curl, dll)
  □ Deteksi banyak email dari satu IP dalam window singkat
  □ Deteksi login dari lokasi yang tidak biasa untuk akun tersebut

CAPTCHA:
  □ CAPTCHA atau challenge setelah N kali gagal
  □ CAPTCHA tidak memblokir user jika service CAPTCHA down
  □ CAPTCHA bukan satu-satunya perlindungan (kombinasikan dengan rate limit)

MFA:
  □ MFA tersedia dan dianjurkan ke semua user
  □ MFA wajib untuk akun admin dan akun dengan privilege tinggi
  □ Brute force MFA code juga di-rate-limit

MONITORING:
  □ Alert untuk login failure rate yang anomalous
  □ Alert untuk banyak email berbeda dari satu IP
  □ Alert untuk login berhasil dari lokasi yang sangat tidak biasa
  □ Dashboard real-time untuk login metrics
  □ Log semua failed attempt dengan detail (IP, UA, timestamp, email hash)

Ringkasan #

  • Credential stuffing adalah ancaman terbesar — bukan mencoba password acak, tapi menggunakan miliaran credential yang sudah bocor. Success rate kecil tapi volumenya sangat besar.
  • Rate limiting harus di dua dimensi — per IP saja tidak cukup karena credential stuffing menggunakan ribuan IP. Rate limit per akun adalah perlindungan yang lebih penting.
  • Progressive lockout lebih baik dari hard lockout — hard lockout bisa di-abuse untuk mengunci akun orang lain. Progressive delay (30 detik, 5 menit, 30 menit) memberikan perlindungan tanpa jadi senjata DoS.
  • Timing attack membocorkan informasi — perbedaan waktu antara “email tidak ada” dan “password salah” memungkinkan enumeration email. Selalu jalankan hash computation bahkan ketika email tidak ditemukan.
  • Pesan error yang seragam wajib — “Email atau password tidak valid” untuk semua kasus. Jangan pernah bedakan “email tidak terdaftar” dari “password salah”.
  • HIBP check proaktif mencegah reuse credential yang bocor — menolak password yang ada di database breach lebih efektif dari semua policy kompleksitas lainnya.
  • MFA adalah mitigasi paling efektif — bahkan jika password berhasil ditebak, attacker masih perlu faktor kedua. Credential stuffing hampir seluruhnya tidak efektif terhadap akun dengan MFA.
  • Deteksi pola credential stuffing — banyak email berbeda dari satu IP, User-Agent yang tidak biasa, header browser yang missing, dan lokasi yang tidak mungkin adalah sinyal yang bisa dideteksi.
  • CAPTCHA bukan solusi sendirian — CAPTCHA farm bisa menyelesaikan CAPTCHA dengan harga $1/1000. Gunakan sebagai salah satu lapisan, bukan satu-satunya.
  • Monitoring real-time adalah kunci — brute force yang berjalan berhari-hari tanpa terdeteksi bisa mengkompromikan ribuan akun sebelum ada yang sadar.

← Sebelumnya: DDoS   Berikutnya: Encryption →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact