Skip to content

Commit 1d56e8b

Browse files
author
teycir
committed
feat(security): add browser fingerprinting for rate limiting
Enhance API endpoint security by implementing browser fingerprinting into the rate limiting mechanism. This generates a unique identifier from IP, User-Agent, and Accept-Language headers, making it harder to bypass rate limits via IP rotation or VPNs.
1 parent be5b8b8 commit 1d56e8b

5 files changed

Lines changed: 22 additions & 7 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ sequenceDiagram
175175
- Key A (in the URL hash)
176176
- Key B (server releases only after unlock time)
177177

178+
### "Can I bypass rate limits by rotating IPs or using VPNs?"
179+
**⚠️ HARDER.** Rate limiting uses browser fingerprinting (IP + User-Agent + Language), making simple IP rotation ineffective. You'd need to change your entire browser signature.
180+
178181
### "Can I replay old API requests to trick the server?"
179182
**❌ NO.** Pulse tokens include nonces and timestamps. Replay attacks are detected and rejected.
180183

@@ -200,6 +203,7 @@ sequenceDiagram
200203
* **Database:** `Cloudflare D1` (SQLite)
201204
* **Storage:** `Cloudflare D1` (Encrypted Blobs)
202205
* **Crypto:** `Web Crypto API` (Native AES-GCM)
206+
* **Security:** Browser Fingerprinting, Rate Limiting, Input Validation
203207
* **Styling:** `Tailwind CSS` (Cipher-punk Theme)
204208

205209
---

app/security/page.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ export default function SecurityPage() {
7676
<p>The server will not release Key B before the unlock time. Server-side validation using Date.now() prevents client-side time manipulation.</p>
7777
</div>
7878
<div>
79-
<p className="text-neon-green font-bold mb-2 flex items-center gap-2"><CheckCircle2 className="w-4 h-4" /> Rate Limiting</p>
80-
<p>API endpoints are rate-limited to 10-20 requests per minute per IP address to prevent brute-force attacks.</p>
79+
<p className="text-neon-green font-bold mb-2 flex items-center gap-2"><CheckCircle2 className="w-4 h-4" /> Rate Limiting with Fingerprinting</p>
80+
<p>API endpoints use browser fingerprinting (IP + User-Agent + Language) to prevent VPN/IP rotation bypass. 10-20 requests per minute per fingerprint.</p>
8181
</div>
8282
<div>
8383
<p className="text-neon-green font-bold mb-2 flex items-center gap-2"><CheckCircle2 className="w-4 h-4" /> No Single Point of Failure</p>
@@ -110,7 +110,8 @@ export default function SecurityPage() {
110110
<li>Client-side time manipulation (server validates with Date.now())</li>
111111
<li>Server compromise (split-key architecture)</li>
112112
<li>Data tampering (WORM storage + AEAD)</li>
113-
<li>Brute force attacks (256-bit keys + rate limiting)</li>
113+
<li>Brute force attacks (256-bit keys + fingerprinted rate limiting)</li>
114+
<li>IP rotation bypass (browser fingerprinting)</li>
114115
<li>Automated abuse (Turnstile CAPTCHA)</li>
115116
<li>Replay attacks (nonce validation on pulse tokens)</li>
116117
</ul>

lib/rateLimit.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// Rate Limiting Middleware for Cloudflare Workers
22
export interface RateLimitConfig {
33
limit: number;
4-
window: number; // milliseconds
4+
window: number;
5+
key?: string;
56
}
67

78
export class RateLimiter {
@@ -84,12 +85,12 @@ export async function withRateLimit(
8485
handler: () => Promise<Response>,
8586
config: RateLimitConfig = { limit: 10, window: 60000 }
8687
): Promise<Response> {
87-
const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
88+
const identifier = config.key || request.headers.get('CF-Connecting-IP') || 'unknown';
8889

8990
const key = `${config.limit}:${config.window}`;
9091
const limiter = RateLimiterRegistry.getInstance().getLimiter(key, config);
9192

92-
const { allowed, remaining } = await limiter.check(ip);
93+
const { allowed, remaining } = await limiter.check(identifier);
9394

9495
if (!allowed) {
9596
return new Response(

lib/routeHelper.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getCloudflareContext } from '@opennextjs/cloudflare';
33
import { createContainer } from './container';
44
import { createHandler, HandlerContext } from './apiHandler';
55
import { withRateLimit } from './rateLimit';
6+
import { getRequestFingerprint } from './security';
67

78
interface RouteHandlerOptions {
89
rateLimit?: { limit: number; window: number };
@@ -19,6 +20,7 @@ export function createAPIRoute(
1920
) {
2021
return async (request: NextRequest, routeParams?: any) => {
2122
const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
23+
const fingerprint = getRequestFingerprint(request);
2224

2325
const wrappedHandler = async () => {
2426
const apiHandler = createHandler(async (ctx: HandlerContext) => {
@@ -32,7 +34,7 @@ export function createAPIRoute(
3234
};
3335

3436
if (options.rateLimit) {
35-
return withRateLimit(request, wrappedHandler, options.rateLimit);
37+
return withRateLimit(request, wrappedHandler, { ...options.rateLimit, key: fingerprint });
3638
}
3739

3840
return wrappedHandler();

lib/security.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@ export async function validatePulseToken(token: string, sealId: string, secret:
119119
return await crypto.subtle.verify('HMAC', key, sigBytes, encoder.encode(data));
120120
}
121121

122+
export function getRequestFingerprint(request: Request): string {
123+
const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
124+
const ua = request.headers.get('user-agent') || '';
125+
const lang = request.headers.get('accept-language') || '';
126+
return `${ip}:${ua.slice(0, 50)}:${lang.slice(0, 20)}`;
127+
}
128+
122129
export function sanitizeError(error: unknown): string {
123130
if (process.env.NODE_ENV === 'production') {
124131
return 'An error occurred. Please try again.';

0 commit comments

Comments
 (0)