Skip to content

security(auth): add rate limiting for brute-force protection#28

Open
DeryFerd wants to merge 8 commits into
anvie:mainfrom
DeryFerd:security/add-login-rate-limiting
Open

security(auth): add rate limiting for brute-force protection#28
DeryFerd wants to merge 8 commits into
anvie:mainfrom
DeryFerd:security/add-login-rate-limiting

Conversation

@DeryFerd
Copy link
Copy Markdown
Contributor

Problem

The login endpoint currently has Cloudflare Turnstile CAPTCHA but no rate limiting. This means attackers can attempt multiple passwords per CAPTCHA solve, making brute-force attacks feasible against weak passwords. While CAPTCHA helps, it's not enough on its own—rate limiting provides defense-in-depth.

What Changed

Added a thread-safe rate limiter that tracks login attempts per IP address using a sliding window algorithm. The /login endpoint now blocks requests after 5 failed attempts within a 5-minute window.

New files:

  • backend/rate_limiter.py — Core rate limiting implementation with configurable limits
  • unit_tests/test_rate_limiter.py — 12 test cases covering edge cases and thread safety

Modified files:

  • routes/auth.py — Applied @rate_limit decorator to login endpoint

The rate limiter uses in-memory storage (good enough for single-instance deployments) and automatically cleans up expired timestamps. For multi-instance setups, this could be extended to use Redis or similar, but that's out of scope for now.

How It Works

When a user hits the login endpoint:

  1. Rate limiter checks their IP address against the sliding window
  2. If they're under the limit (5 attempts in 5 minutes), request proceeds normally
  3. If they've exceeded the limit, they get a 429 response with a clear error message
  4. The window slides continuously—no hard resets at fixed intervals

The implementation is thread-safe using locks, so concurrent requests from the same IP won't race and bypass the limit.

Testing

All 12 unit tests pass, covering:

  • Basic allow/block behavior
  • Sliding window expiration
  • Independent limits per IP
  • Remaining attempts tracking
  • Thread safety under concurrent load
  • Decorator integration with Flask routes

Manually tested the login flow:

  • First 5 attempts work normally (with wrong password)
  • 6th attempt gets blocked with "Too many login attempts" message
  • After waiting 5 minutes, can attempt again

Compatibility

This is backward compatible—existing login behavior is unchanged for users who don't hit the limit. No database migrations needed since rate limit data is in-memory.

The rate limiter is generic enough to be reused on other endpoints if needed (API routes, password reset, etc.), though that's not part of this PR.

DeryFerd added 3 commits May 14, 2026 21:59
- Add RateLimiter class with sliding window algorithm
- Apply rate limiting to /login endpoint (5 attempts per 5 minutes)
- Thread-safe implementation with proper locking
- Comprehensive unit tests with 12 test cases
- Support for custom identifier functions
- JSON and HTML error responses
- Move config import inside rate_limit decorator
- Prevents issues when tests reload config module
- Fixes failing auth open redirect tests in CI
@DeryFerd DeryFerd force-pushed the security/add-login-rate-limiting branch from 4b21bee to b68484f Compare May 14, 2026 16:09
@anvie
Copy link
Copy Markdown
Owner

anvie commented May 15, 2026

There is too many unrelated changes for this

@DeryFerd
Copy link
Copy Markdown
Contributor Author

DeryFerd commented May 15, 2026

@anvie Thanks for the feedback. I just pushed commit 0040afd to fix the failing CI (rate limiter custom identifier path), and both checks are now passing on run 25902803580.

You’re right that this PR currently includes unrelated files. I can immediately follow up by trimming this branch to only rate-limiter/auth/test changes (or split unrelated work into a separate PR), whichever you prefer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants