Medium CWE-799 OWASP A04:2021

Missing Rate Limiting in Vibe Coded Apps

Why AI tools never add rate limiting and how to protect your login endpoints from brute force attacks

Quick Answer

Rate limiting caps how many requests a user can make in a timeframe. Without it, attackers can try millions of passwords per second on your login page. AI tools never add rate limiting because it is not required for code to "work."

API4:2023
OWASP API Risk
CWE-799
Millions/sec
Attack Speed
Medium
Severity

Sources: OWASP API Security 2023, OWASP Top 10 (2021)

What is rate limiting?

Rate limiting is a security control that restricts how many requests a user or IP address can make to your API within a specific timeframe. Think of it like a bouncer at a club who only lets five people in per minute - even if a crowd rushes the door, the flow stays controlled.

Without rate limiting, your API endpoints are completely open. According to Snyk Learn, brute force attacks can test "tens of thousands of passwords per second, all the way up to hundreds of millions" against unprotected login endpoints.

This vulnerability is classified as CWE-799: Improper Control of Interaction Frequency and appears in OWASP API Security Top 10 as API4:2023 (Unrestricted Resource Consumption). For vibe coders using AI tools, this is especially critical because AI never adds rate limiting unless you explicitly ask for it.

How do attackers exploit missing rate limiting?

Attackers exploit missing rate limiting through brute force attacks, credential stuffing, and denial of service. Without limits, your server will happily process every single request.

The brute force attack

  1. Attacker finds your login endpoint - usually /api/auth/login or similar
  2. Runs automated script - tries common passwords against known usernames
  3. Millions of attempts per hour - without rate limiting, nothing stops them
  4. Eventually succeeds - weak passwords fall quickly to dictionary attacks

With rate limiting at 5 attempts per 15 minutes, this attack would take years. Without it, it takes hours or minutes.

According to OWASP, brute force attacks remain one of the most common attack vectors because they work. The solution is defense in depth: rate limiting combined with CAPTCHA, account lockout, and multi-factor authentication.

Where does rate limiting matter most?

Rate limiting is critical on any endpoint that could be abused through repeated requests. Some endpoints need strict limits while others can be more permissive.

Critical
Login endpoints - brute force password attacks
Critical
Password reset - account takeover via email flooding
Critical
Registration - spam account creation
High
API endpoints - resource exhaustion, cost attacks
High
File uploads - storage exhaustion
Medium
Search/filtering - database load attacks

How do AI tools create vulnerable code?

AI tools create vulnerable code because rate limiting is not required for functionality. When you ask for a login endpoint, AI generates code that authenticates users - that is the complete request. Security features like rate limiting are "extra."

Common AI-generated vulnerable pattern

When you ask vibe coding tools like Cursor, Bolt, or Claude Code for a login endpoint, they generate this:

// AI-generated login - no rate limiting
// app/api/auth/login/route.ts
import { NextResponse } from 'next/server'
import { verifyCredentials } from '@/lib/auth'

export async function POST(request: Request) {
  const { email, password } = await request.json()

  // Attacker can call this endpoint millions of times!
  const user = await verifyCredentials(email, password)

  if (!user) {
    return NextResponse.json(
      { error: 'Invalid credentials' },
      { status: 401 }
    )
  }

  return NextResponse.json({ user })
}

This code works perfectly for legitimate users. But there is nothing stopping an attacker from calling it millions of times per second with different passwords.

Why this happens: AI tools are trained to fulfill your request efficiently. When vibe coding and you ask for "a login API," it builds exactly that. Rate limiting is a security concern that requires explicit prompting: "Add rate limiting to prevent brute force attacks."

How do I detect missing rate limiting?

Detect missing rate limiting by reviewing your authentication endpoints and checking for rate limit middleware. Routes without rateLimit imports or middleware are vulnerable.

Red flags to look for
// Check these files for rate limiting:
// - app/api/auth/*/route.ts (Next.js App Router)
// - pages/api/auth/*.ts (Next.js Pages Router)
// - routes/auth.ts (Express)
// - middleware.ts (global middleware)

// Red flags:
// 1. No "rateLimit" or "Ratelimit" imports
// 2. No middleware wrapping auth routes
// 3. Direct database/auth calls without limits

// Quick test: Can you call the endpoint 100 times in 1 second?
// If yes, you need rate limiting.

for (let i = 0; i < 100; i++) {
  fetch('/api/auth/login', {
    method: 'POST',
    body: JSON.stringify({ email: '[email protected]', password: 'wrong' })
  })
}
// If all 100 requests complete, you're vulnerable

Scan your endpoints automatically

Scan your code free

How do I add rate limiting?

Add rate limiting using middleware that tracks request counts per IP address or user. The approach differs between traditional servers and serverless environments.

AI Fix Prompt

Copy this prompt into Cursor, Claude Code, or Bolt to automatically add rate limiting to your endpoints:

Copy-paste this prompt
Add rate limiting to all authentication and sensitive API endpoints in my codebase. ## What to look for Search for endpoints that need rate limiting: 1. Authentication endpoints: - app/api/auth/**/route.ts (Next.js App Router) - pages/api/auth/*.ts (Next.js Pages Router) - Login, register, password reset, magic link routes 2. Sensitive API endpoints: - Any route that modifies data - Search or filter endpoints (database load) - File upload endpoints 3. Check for existing rate limiting: - Look for "rateLimit" or "Ratelimit" imports - Check middleware.ts for global rate limiting ## How to fix ### For Next.js App Router (Serverless - use Upstash): ```typescript // middleware.ts import { Ratelimit } from '@upstash/ratelimit' import { Redis } from '@upstash/redis' import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(5, '15 m'), // 5 requests per 15 minutes analytics: true, }) export async function middleware(request: NextRequest) { if (request.nextUrl.pathname.startsWith('/api/auth')) { const ip = request.ip ?? request.headers.get('x-forwarded-for') ?? '127.0.0.1' const { success, limit, reset, remaining } = await ratelimit.limit(ip) if (!success) { return NextResponse.json( { error: 'Too many requests. Please try again later.' }, { status: 429, headers: { 'X-RateLimit-Limit': limit.toString(), 'X-RateLimit-Remaining': remaining.toString(), 'X-RateLimit-Reset': reset.toString(), }, } ) } } return NextResponse.next() } export const config = { matcher: '/api/:path*', } ``` ### For Express: ```typescript import rateLimit from 'express-rate-limit' // IMPORTANT: Define OUTSIDE handler functions! const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 attempts per window message: { error: 'Too many login attempts. Please try again later.' }, standardHeaders: true, legacyHeaders: false, }) const apiLimiter = rateLimit({ windowMs: 60 * 1000, // 1 minute max: 100, // 100 requests per minute message: { error: 'Rate limit exceeded.' }, }) // Apply to routes app.use('/api/auth', authLimiter) app.use('/api', apiLimiter) ``` ### For Next.js Pages Router: ```typescript import rateLimit from 'express-rate-limit' // Define OUTSIDE the handler const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, }) export default function handler(req, res) { return new Promise((resolve) => { limiter(req, res, () => { // Your login logic here resolve() }) }) } ``` ## Rate limit recommendations - Login/auth endpoints: 5 requests per 15 minutes - Password reset: 3 requests per hour - API endpoints: 100 requests per minute - File uploads: 10 per hour ## After fixing 1. Test by making rapid requests - should get 429 after limit 2. Verify rate limit headers are returned (X-RateLimit-*) 3. Install dependencies: npm install @upstash/ratelimit @upstash/redis (serverless) or npm install express-rate-limit (Express) 4. For Upstash, add UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN to .env 5. List all routes you modified Please proceed systematically through my codebase.

Manual Fix: Next.js with Upstash (Serverless)

For Vercel and other serverless platforms, use Upstash Rate Limit because it stores state in Redis across function instances.

VULNERABLE
// No rate limiting - anyone can spam
export async function POST(request: Request) {
  const { email, password } = await request.json()

  // Can be called millions of times
  const user = await verifyCredentials(email, password)

  if (!user) {
    return NextResponse.json(
      { error: 'Invalid credentials' },
      { status: 401 }
    )
  }

  return NextResponse.json({ user })
}
SECURE
// middleware.ts - rate limits all auth routes
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(5, '15 m'),
})

export async function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/auth')) {
    const ip = request.ip ?? '127.0.0.1'
    const { success } = await ratelimit.limit(ip)

    if (!success) {
      return NextResponse.json(
        { error: 'Too many requests' },
        { status: 429 }
      )
    }
  }
  return NextResponse.next()
}

Manual Fix: Express with express-rate-limit

For traditional Node.js servers, use express-rate-limit. Critical: define the limiter OUTSIDE handler functions.

WRONG - Limiter inside handler
// BAD: Creates new limiter per request!
app.post('/api/login', async (req, res) => {
  // This limiter resets every request
  const limiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 5,
  })

  limiter(req, res, async () => {
    // Login logic
  })
})
CORRECT - Limiter outside handler
import rateLimit from 'express-rate-limit'

// Define ONCE, outside any handler
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 attempts
  message: { error: 'Too many attempts' },
  standardHeaders: true,
})

// Use as middleware
app.post('/api/login', loginLimiter, async (req, res) => {
  // Login logic - only runs if under limit
  const user = await verifyCredentials(req.body)
  res.json({ user })
})

Beyond rate limiting: defense in depth

Rate limiting slows attacks but does not stop determined attackers. According to OWASP, combine rate limiting with additional measures for robust protection.

  • CAPTCHA after failed attempts: Show CAPTCHA after 3 failed logins. This stops automated attacks while allowing legitimate users to retry.
  • Account lockout (with caution): Lock accounts after 10 failed attempts. Caution: this can be used for denial-of-service against specific users.
  • Exponential backoff: Increase wait time after each failure: 1s, 2s, 4s, 8s... Makes brute force exponentially slower.
  • Multi-factor authentication: Even if password is guessed, attacker needs the second factor. This is the strongest protection.
  • Breach password checking: Check passwords against known breach databases (haveibeenpwned.com API) to reject commonly compromised passwords.

Frequently asked questions

What happens without rate limiting?

Without rate limiting, attackers can make unlimited requests to your API. On login endpoints, this means trying millions of passwords per second. According to Snyk, brute force attacks can test "hundreds of millions" of passwords without rate limiting. Your server also becomes vulnerable to denial-of-service attacks.

How do I add rate limiting to Next.js?

For Next.js App Router, use middleware with Upstash Rate Limit for serverless-friendly limiting that persists across function instances. For Pages Router, use express-rate-limit but define the limiter OUTSIDE the handler function. Otherwise, each request creates a fresh limiter with no memory of previous requests.

Is rate limiting enough to prevent brute force attacks?

No, rate limiting alone is not enough. OWASP recommends combining rate limiting with CAPTCHA after failed attempts, account lockout policies, exponential backoff, and multi-factor authentication. Rate limiting slows attacks; these additional measures stop automated attacks entirely.

What is a good rate limit for login endpoints?

For login endpoints, 5 attempts per 15 minutes is a common starting point. This allows legitimate users who mistype passwords while making brute force attacks impractical. Adjust based on your user base - corporate networks with shared IPs may need higher limits with additional verification like CAPTCHA.

Does Vercel have built-in rate limiting?

Vercel does not have built-in rate limiting for your API routes. Since Vercel functions are serverless and stateless, you need an external store like Upstash Redis to track request counts across function instances. The Upstash Rate Limit SDK is designed specifically for this use case.

Related content

Scan your code for missing rate limiting

Check your authentication endpoints for rate limiting and other common security vulnerabilities.

Try vibeship scanner