BillTracker/docs/RATE_LIMITING_ENHANCEMENT.md

143 lines
3.8 KiB
Markdown
Raw Normal View History

2026-05-09 13:03:36 -05:00
# Rate Limiting Enhancement — Future Consideration
**Date:** 2026-05-08
**Status:** Documented for future implementation
**Priority:** Low (current per-IP limits are functional for self-hosted use)
---
## Current Implementation
All rate limiters in `middleware/rateLimiter.js` use **per-IP** limiting:
```javascript
keyGenerator: (req) => req.ip
```
### Current Limits
| Limiter | Limit | Keyed By |
|---------|-------|----------|
| `importLimiter` | 20 per 15 min | IP |
| `exportLimiter` | 30 per 15 min | IP |
| `adminActionLimiter` | 30 per 15 min | IP |
| `demoDataLimiter` | 3 per 15 min | IP |
| `backupOperationLimiter` | 5 per hour | IP |
| `loginLimiter` | 5 per 15 min | IP |
| `passwordLimiter` | 3 per hour | IP |
| `oidcLimiter` | 10 per 15 min | IP |
---
## Limitations of Per-IP Limiting
| Scenario | Problem |
|----------|---------|
| Shared office network | All users share one rate limit bucket |
| VPN users | Multiple users behind same IP share limit |
| Same user, multiple devices | Separate limits per device (may be desired) |
| Malicious actor with IP rotation | Can bypass limits by rotating IPs |
---
## Recommended Enhancement: Hybrid Per-User + Per-IP
### Implementation
```javascript
// middleware/rateLimiter.js
const hybridKeyGenerator = (req) => {
// Authenticated users get per-user limits
if (req.user?.id) {
return `user:${req.user.id}`;
}
// Anonymous requests get per-IP limits
return `ip:${req.ip}`;
};
// Apply to existing limiters
const importLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 20,
keyGenerator: hybridKeyGenerator, // <-- Changed
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
res.status(429).json({
error: 'Too many import requests. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
retryAfter: Math.ceil(req.rateLimit.resetTime / 1000),
});
},
});
```
### Benefits
| Benefit | Explanation |
|---------|-------------|
| Fair per-user allocation | Each user gets their own rate limit bucket |
| Shared network friendly | Office/VPN users don't share limits |
| Abuse resistant | Can't bypass by IP rotation if authenticated |
| Backwards compatible | Falls back to IP for anonymous requests |
---
## Alternative: Separate Limits for Auth vs Anonymous
```javascript
const createHybridLimiter = (authMax, anonMax, windowMs) => {
const authLimiter = rateLimit({
windowMs,
max: authMax,
keyGenerator: (req) => req.user?.id ? `user:${req.user.id}` : 'anonymous',
skip: (req) => !req.user?.id, // Skip if not authenticated
});
const anonLimiter = rateLimit({
windowMs,
max: anonMax,
keyGenerator: (req) => `ip:${req.ip}`,
skip: (req) => !!req.user?.id, // Skip if authenticated
});
return (req, res, next) => {
if (req.user?.id) {
return authLimiter(req, res, next);
}
return anonLimiter(req, res, next);
};
};
// Usage: authenticated users get 50/15min, anonymous get 20/15min
const importLimiter = createHybridLimiter(50, 20, 15 * 60 * 1000);
```
---
## When to Implement
| Trigger | Action |
|---------|--------|
| Multi-tenant deployment | Higher priority — many users sharing infrastructure |
| API key authentication | Required — API keys need per-key limits |
| Public cloud hosting | Recommended — shared IPs common |
| Self-hosted, small team | Low priority — current per-IP is adequate |
---
## Related Files
- `middleware/rateLimiter.js` — Current rate limiter definitions
- `server.js` — Rate limiter application (routes)
- `docs/SECURITY.md` — Security documentation
---
## Notes
- Current per-IP limits are appropriate for self-hosted SQLite deployment
- Hybrid approach should be considered if adding API keys or multi-tenant support
- Rate limit storage is in-memory (Redis not required for current scale)