176 lines
4.8 KiB
Markdown
176 lines
4.8 KiB
Markdown
---
|
|
title: Enable Rate Limiting for API Protection
|
|
impact: MEDIUM
|
|
impactDescription: Prevents abuse, brute-force attacks, and DoS
|
|
tags: production, security, rate-limiting, abuse-prevention
|
|
---
|
|
|
|
## Enable Rate Limiting for API Protection
|
|
|
|
PocketBase v0.23+ includes built-in rate limiting. Enable it to protect against brute-force attacks, API abuse, and excessive resource consumption.
|
|
|
|
> **v0.36.7 behavioral change:** the built-in limiter switched from a sliding-window to a **fixed-window** strategy. This is cheaper and more predictable, but it means a client can send `2 * maxRequests` in quick succession if they straddle the window boundary. Size your limits with that worst case in mind, and put Nginx/Caddy rate limiting in front of PocketBase for defense in depth (see examples below).
|
|
|
|
**Incorrect (no rate limiting):**
|
|
|
|
```bash
|
|
# Running without rate limiting
|
|
./pocketbase serve
|
|
|
|
# Vulnerable to:
|
|
# - Brute-force password attacks
|
|
# - API abuse and scraping
|
|
# - DoS from excessive requests
|
|
# - Account enumeration attempts
|
|
```
|
|
|
|
**Correct (enable rate limiting):**
|
|
|
|
```bash
|
|
# Enable via command line flag
|
|
./pocketbase serve --rateLimiter=true
|
|
|
|
# Or configure specific limits (requests per second per IP)
|
|
./pocketbase serve --rateLimiter=true --rateLimiterRPS=10
|
|
```
|
|
|
|
**Configure via Admin Dashboard:**
|
|
|
|
Navigate to Settings > Rate Limiter:
|
|
- **Enable rate limiter**: Toggle on
|
|
- **Max requests/second**: Default 10, adjust based on needs
|
|
- **Exempt endpoints**: Optionally whitelist certain paths
|
|
|
|
**Configure programmatically (Go/JS hooks):**
|
|
|
|
```javascript
|
|
// In pb_hooks/rate_limit.pb.js
|
|
routerAdd("GET", "/api/public/*", (e) => {
|
|
// Custom rate limit for specific endpoints
|
|
}, $apis.rateLimit(100, "10s")); // 100 requests per 10 seconds
|
|
|
|
// Stricter limit for auth endpoints
|
|
routerAdd("POST", "/api/collections/users/auth-*", (e) => {
|
|
// Auth endpoints need stricter limits
|
|
}, $apis.rateLimit(5, "1m")); // 5 attempts per minute
|
|
```
|
|
|
|
**Rate limiting with reverse proxy (additional layer):**
|
|
|
|
```nginx
|
|
# Nginx rate limiting (defense in depth)
|
|
http {
|
|
# Define rate limit zones
|
|
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
|
limit_req_zone $binary_remote_addr zone=auth:10m rate=1r/s;
|
|
|
|
server {
|
|
# General API rate limit
|
|
location /api/ {
|
|
limit_req zone=api burst=20 nodelay;
|
|
proxy_pass http://pocketbase;
|
|
}
|
|
|
|
# Strict limit for auth endpoints
|
|
location /api/collections/users/auth {
|
|
limit_req zone=auth burst=5 nodelay;
|
|
proxy_pass http://pocketbase;
|
|
}
|
|
|
|
# Stricter limit for superuser auth
|
|
location /api/collections/_superusers/auth {
|
|
limit_req zone=auth burst=3 nodelay;
|
|
proxy_pass http://pocketbase;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
```caddyfile
|
|
# Caddy with rate limiting plugin
|
|
myapp.com {
|
|
rate_limit {
|
|
zone api {
|
|
key {remote_host}
|
|
events 100
|
|
window 10s
|
|
}
|
|
zone auth {
|
|
key {remote_host}
|
|
events 5
|
|
window 1m
|
|
}
|
|
}
|
|
|
|
@auth path /api/collections/*/auth*
|
|
handle @auth {
|
|
rate_limit { zone auth }
|
|
reverse_proxy 127.0.0.1:8090
|
|
}
|
|
|
|
handle {
|
|
rate_limit { zone api }
|
|
reverse_proxy 127.0.0.1:8090
|
|
}
|
|
}
|
|
```
|
|
|
|
**Handle rate limit errors in client:**
|
|
|
|
```javascript
|
|
async function makeRequest(fn, retries = 0, maxRetries = 3) {
|
|
try {
|
|
return await fn();
|
|
} catch (error) {
|
|
if (error.status === 429 && retries < maxRetries) {
|
|
// Rate limited - wait and retry with limit
|
|
const retryAfter = error.response?.retryAfter || 60;
|
|
console.log(`Rate limited. Retry ${retries + 1}/${maxRetries} after ${retryAfter}s`);
|
|
|
|
// Show user-friendly message
|
|
showMessage('Too many requests. Please wait a moment.');
|
|
|
|
await sleep(retryAfter * 1000);
|
|
return makeRequest(fn, retries + 1, maxRetries);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
const result = await makeRequest(() =>
|
|
pb.collection('posts').getList(1, 20)
|
|
);
|
|
```
|
|
|
|
**Recommended limits by endpoint type:**
|
|
|
|
| Endpoint Type | Suggested Limit | Reason |
|
|
|--------------|-----------------|--------|
|
|
| Auth endpoints | 5-10/min | Prevent brute-force |
|
|
| Password reset | 3/hour | Prevent enumeration |
|
|
| Record creation | 30/min | Prevent spam |
|
|
| General API | 60-100/min | Normal usage |
|
|
| Public read | 100-200/min | Higher for reads |
|
|
| File uploads | 10/min | Resource-intensive |
|
|
|
|
**Monitoring rate limit hits:**
|
|
|
|
```javascript
|
|
// Check PocketBase logs for rate limit events
|
|
// Or set up alerting in your monitoring system
|
|
|
|
// Client-side tracking
|
|
pb.afterSend = function(response, data) {
|
|
if (response.status === 429) {
|
|
trackEvent('rate_limit_hit', {
|
|
endpoint: response.url,
|
|
timestamp: new Date()
|
|
});
|
|
}
|
|
return data;
|
|
};
|
|
```
|
|
|
|
Reference: [PocketBase Going to Production](https://pocketbase.io/docs/going-to-production/)
|