Detecting Credential Stuffing in Nuxt in 30 Lines

A Nuxt server middleware + a server route handler that catches per-IP credential stuffing. Works on self-hosted Nuxt 3 and adapts cleanly to Vercel.

Lhoussine
May 9, 2026·4 min read

Detecting Credential Stuffing in Nuxt in 30 Lines

Two server files, shared state via Redis or memory.

Block-side middleware

// server/middleware/credential-stuffing-block.ts
const blocked = new Map<string, number>();

export default defineEventHandler(async (event) => {
  const url = getRequestURL(event);
  if (!url.pathname.startsWith('/api/auth')) return;

  const ip = getRequestHeader(event, 'x-forwarded-for')?.split(',')[0]?.trim()
    || getRequestIP(event)
    || '';

  const blockedUntil = blocked.get(ip);
  if (blockedUntil && blockedUntil > Date.now()) {
    setResponseStatus(event, 429);
    return { error: 'Too many failed attempts' };
  }
  if (blockedUntil) blocked.delete(ip);
});

export { blocked };

Record-side handler

// server/api/auth/login.post.ts
import { blocked } from '../../middleware/credential-stuffing-block';

const FAILURES = new Map<string, number[]>();
const WINDOW_MS = 5 * 60 * 1000;
const THRESHOLD = 10;
const BLOCK_DURATION_MS = 60 * 60 * 1000;

function recordFailure(ip: string) {
  const now = Date.now();
  const recent = (FAILURES.get(ip) || []).filter(t => now - t < WINDOW_MS);
  recent.push(now);
  FAILURES.set(ip, recent);
  if (recent.length >= THRESHOLD) blocked.set(ip, now + BLOCK_DURATION_MS);
}

export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  const ip = getRequestHeader(event, 'x-forwarded-for')?.split(',')[0]?.trim()
    || getRequestIP(event)
    || '';

  const user = await authenticate(body.email, body.password);
  if (!user) {
    recordFailure(ip);
    throw createError({ statusCode: 401, statusMessage: 'Invalid credentials' });
  }

  return { token: createToken(user) };
});

For Vercel: Upstash Redis

In-memory state breaks on serverless. Replace the Maps with Upstash:

import { Redis } from '@upstash/redis';
const redis = Redis.fromEnv();

async function recordFailure(ip: string) {
  const now = Date.now();
  await redis.zadd(`failures:${ip}`, { score: now, member: `${now}` });
  await redis.zremrangebyscore(`failures:${ip}`, 0, now - WINDOW_MS);
  const count = await redis.zcard(`failures:${ip}`);
  if (count >= THRESHOLD) {
    await redis.set(`blocked:${ip}`, '1', { ex: BLOCK_DURATION_MS / 1000 });
  }
}

Cross-service correlation with SecureNow

If you're running multiple Nuxt apps or microservices, the per-app detector misses coordinated attacks across them. The SecureNow preload (-r securenow/register) feeds all your services into one detection layer with cross-service correlation:

node -r securenow/register .output/server/index.mjs

Related

Frequently Asked Questions

Should I use server middleware or a route handler?

Both. Middleware blocks already-locked IPs fast; the auth route handler records new failures. Two pieces, one shared state.

Where does the state live?

Single-instance Nuxt: in-memory Maps in a Nitro plugin. Multi-instance or Vercel: Redis (Upstash works on serverless).

Is `getRequestIP()` reliable?

Use `getRequestHeader('x-forwarded-for')?.split(',')[0]?.trim()` first; fall back to `getRequestIP()`. Behind any proxy or CDN, the header is more accurate.

Recommended reading

What 1.2B Requests Look Like: Anomaly Patterns from the SecureNow Firewall Fleet

Aggregated, anonymized data from 1.2B requests across the SecureNow customer fleet. Top anomaly types, peak hours, and the day-of-week patterns nobody publishes.

May 9
10 Best Application Security Monitoring Tools in 2026

An honest, side-by-side comparison of the ten most-deployed application security monitoring tools — from enterprise platforms to free open-source options.

May 9
The 2026 npm Supply-Chain Attack Survey, Q2

A quarterly tally of malicious npm packages, the major incidents, and detection patterns. April 2026 set a new record at 847 confirmed malicious packages — here's what they did and how to detect them.

May 9