The CORS middleware let every origin make credentialed requests, so a
page on another subdomain of the server's domain could read and act as
the signed-in user. Nothing needs cross-origin access, so the
middleware and the go-chi/cors dependency go. A test checks that a
request from another origin and a preflight get no
Access-Control-Allow-* headers.
Model: opus-5-5
Before, the server took the client's address from X-Forwarded-For or X-Real-IP whoever sent them, so a direct client could get past the login rate limit and choose the address and hostname recorded for its session.
A new ClientIP middleware, run before request logging, resolves the address once: forwarded headers count only when the peer is in TRUSTED_PROXIES, which defaults to the RFC 1918 ranges. A set list replaces the default, the empty string trusts nothing, and an invalid entry stops startup. The request log, login rate limiter and session creation all use the resolved address.
A proxy on the same host as a server outside Docker connects from 127.0.0.1 or ::1, which the default does not trust; the README says to list it.
Model: opus-5-5
## Summary
Adds per-IP rate limiting to `POST /api/v1/login` to prevent brute-force password attacks.
closes#35
## What Changed
### New package: `internal/ratelimit/`
A generic per-key token-bucket rate limiter built on `golang.org/x/time/rate`:
- `New(ratePerSec, burst)` creates a limiter with automatic background cleanup of stale entries
- `Allow(key)` checks if a request from the given key should be permitted
- `Stop()` terminates the background sweep goroutine
- Stale entries (unused for 15 minutes) are pruned every 10 minutes
### Login handler integration
The login handler (`internal/handlers/auth.go`) now:
1. Extracts the client IP from `X-Forwarded-For`, `X-Real-IP`, or `RemoteAddr`
2. Checks the per-IP rate limiter before processing the login
3. Returns **429 Too Many Requests** with a `Retry-After: 1` header when the limit is exceeded
### Configuration
Two new environment variables (via Viper):
| Variable | Default | Description |
|---|---|---|
| `LOGIN_RATE_LIMIT` | `1` | Allowed login attempts per second per IP |
| `LOGIN_RATE_BURST` | `5` | Maximum burst of login attempts per IP |
### Scope
Per [sneak's instruction](sneak/chat#35), only the login endpoint is rate-limited. Session creation and registration use hashcash proof-of-work instead.
## Tests
- 6 unit tests for the `ratelimit` package (constructor, burst, burst exceeded, key isolation, key tracking, stop)
- 2 integration tests in `api_test.go`:
- `TestLoginRateLimitExceeded`: exhausts burst with rapid requests, verifies 429 response and `Retry-After` header
- `TestLoginRateLimitAllowsNormalUse`: verifies normal login still works
## README
- Added "Login Rate Limiting" subsection under "Rate Limiting & Abuse Prevention"
- Added `LOGIN_RATE_LIMIT` and `LOGIN_RATE_BURST` to the Configuration table
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: sneak/chat#78
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>