Skip to content

feat(security): Implement sliding-window rate limiting on auth endpoint to prevent brute-force attacks#402

Merged
Aditya948351 merged 3 commits into
devpathindcommunity-india:masterfrom
Niteshagarwal01:fix/auth-rate-limiting
May 31, 2026
Merged

feat(security): Implement sliding-window rate limiting on auth endpoint to prevent brute-force attacks#402
Aditya948351 merged 3 commits into
devpathindcommunity-india:masterfrom
Niteshagarwal01:fix/auth-rate-limiting

Conversation

@Niteshagarwal01
Copy link
Copy Markdown
Contributor

feat(security): Implement sliding-window rate limiting on auth endpoint to prevent brute-force attacks #360

Summary

The /api/auth/verify-admin route accepted unlimited POST requests with no throttling,
allowing an attacker to brute-force the admin key by hammering the endpoint programmatically.
This PR introduces a zero-dependency, in-memory sliding-window rate limiter built natively
for Next.js Route Handlers, capped at 5 attempts per IP per 60 seconds on the
admin key verification endpoint.


Problem

// BEFORE — no rate limiting, unlimited attempts accepted
export async function POST(request: Request) {
    const body = await request.json();
    const { key } = body;
    // key immediately compared against Firestore value
    if (key === actualKey) {
        return NextResponse.json({ success: true });
    }
    return NextResponse.json({ message: 'Invalid Admin Key.' }, { status: 401 });
}

With no throttle in place, a script could submit thousands of guesses per second.
The admin key space — while secret — should never rely solely on key secrecy as
the only line of defence. Each failed attempt also triggered an unnecessary Firestore
read, meaning a sustained attack would simultaneously inflate database read costs.

Note: express-rate-limit cannot be used here — this project runs on Next.js
with no Express server. This PR implements an equivalent solution natively using
Next.js Route Handler primitives.


Solution

Architecture: Sliding Window vs Fixed Window

A fixed-window limiter (e.g. "10 requests per minute, reset at :00") has a known
bypass: an attacker can fire 10 requests at 0:59 and 10 more at 1:01, achieving
20 requests in 2 seconds while staying within the per-minute limit.

The sliding window eliminates this by measuring the last N milliseconds from now,
not from a fixed clock boundary. Every request checks whether the oldest timestamp
in the window has aged out, and the window slides continuously.


New Files

src/lib/rateLimiter.ts

/**
 * Zero-dependency, in-memory sliding-window rate limiter for Next.js Route Handlers.
 * Keyed by IP address.
 */

const store = new Map<string, { timestamps: number[] }>();

export function rateLimit(
  request: Request,
  options: { limit?: number; windowMs?: number; message?: string } = {}
): RateLimitResult | RateLimitBlocked {
  const { limit = 10, windowMs = 60_000, message = 'Too many requests.' } = options;

  const ip = getIp(request);   // x-real-ip → cf-connecting-ip → x-forwarded-for
  const now = Date.now();
  const windowStart = now - windowMs;

  const entry = store.get(ip) ?? { timestamps: [] };
  entry.timestamps = entry.timestamps.filter(t => t > windowStart); // slide

  if (entry.timestamps.length >= limit) {
    const retryAfterSec = Math.ceil((entry.timestamps[0] + windowMs - now) / 1000);
    return {
      success: false,
      response: new Response(JSON.stringify({ success: false, message }), {
        status: 429,
        headers: {
          'Retry-After': String(retryAfterSec),
          'X-RateLimit-Limit': String(limit),
          'X-RateLimit-Remaining': '0',
          'X-RateLimit-Reset': String(Math.ceil((entry.timestamps[0] + windowMs) / 1000)),
        },
      }),
    };
  }

  entry.timestamps.push(now);
  store.set(ip, entry);
  if (Math.random() < 0.01) prune(); // bounded memory

  return { success: true, remaining: limit - entry.timestamps.length };
}

Design decisions:

Decision Rationale
Sliding window Eliminates fixed-window boundary bypass attacks
Module-level Map store Persists across requests in the same warm Next.js server instance — correct behaviour for serverless route handlers
IP header priority chain x-real-ipcf-connecting-ipx-forwarded-for covers Vercel, Cloudflare, and standard reverse proxies
Retry-After header RFC 6585 compliant — tells clients exactly when to retry
X-RateLimit-* headers Industry-standard response headers for rate limit state
1% probabilistic pruning Keeps the in-memory store bounded without a separate GC interval
Zero dependencies No npm install required — no supply-chain surface added

Modified Files

src/app/api/auth/verify-admin/route.ts

// AFTER — rate limiter applied at the top of the handler, before any Firestore read
export async function POST(request: Request) {
    const limit = rateLimit(request, {
        limit: 5,
        windowMs: 60_000,
        message: 'Too many key attempts. Please wait before trying again.',
    });

    if (!limit.success) {
        return limit.response; // 429 with Retry-After, X-RateLimit-* headers
    }

    // ... Firestore read and key comparison only reached if within rate limit
}

Why 5 attempts per 60 seconds?

The admin key is a high-value secret. 5 attempts per minute is generous enough for
a legitimate admin who misremembers their key, while making automated brute-force
infeasible — at 5 req/min, exhausting a 10-character alphanumeric keyspace would
take longer than the age of the universe.

Firestore is also only queried after the rate check passes, so blocked requests
cost zero database reads.


Reusability

The rateLimit utility is fully generic and can protect any future Route Handler
with a single line:

// Any other API route — custom limits per endpoint sensitivity
const check = rateLimit(request, { limit: 20, windowMs: 60_000 });
if (!check.success) return check.response;

Security Impact

Attack Before After
Brute-force key enumeration Unlimited attempts, no throttle Blocked after 5 attempts/min per IP
Distributed brute-force (multiple IPs) Unlimited Mitigated per-IP; can be extended with a global counter
Firestore read amplification under attack Every request hits Firestore Blocked requests never reach Firestore
Missing rate limit headers No indication to client Retry-After, X-RateLimit-Remaining, X-RateLimit-Reset returned

Verification

  1. Send 5 POST requests to /api/auth/verify-admin with an incorrect key in quick succession — all return 401.
  2. Send a 6th request within the same 60-second window — response is 429 with body { success: false, message: "Too many key attempts..." } and headers Retry-After, X-RateLimit-Limit: 5, X-RateLimit-Remaining: 0.
  3. Wait for the Retry-After duration and retry — request is accepted again.
  4. Send a correct key within the limit window — returns { success: true } as expected.
  5. Confirm that no regressions exist for normal admin login flow.

Branch

fix/auth-rate-limitingmaster

@Aditya948351 Aditya948351 merged commit 05d6327 into devpathindcommunity-india:master May 31, 2026
@Aditya948351
Copy link
Copy Markdown
Collaborator

Do star the repo and thanks for the Contribution!

@Aditya948351 Aditya948351 added gssoc26 This is a official GirlScript Summer of Code label. level:intermediate Intermediate level issues type:security gssoc:approved give 50+ base points labels May 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gssoc:approved give 50+ base points gssoc26 This is a official GirlScript Summer of Code label. level:intermediate Intermediate level issues type:security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants