NODE.JS SECURITY SCANNER

Scan Node.js and Express apps for dependency CVEs, unsafe Express configuration, and missing hardening. Built for AI-generated backends that skipped the security pass.

PROBE YOUR NODE.JS APP NOW

Enter your deployed URL — we test runtime config, error handling, rate limits, and dependency exposure. No repo access required.

Why Node.js Apps Need Runtime Testing

Static analysis catches half the problem. The other half only appears when the app is running: what headers does the server send? Which routes skipped requireAuth? What does /error actually return when you send malformed JSON? Does the auth route slow down after 100 hits-per-second, or does it cheerfully serve 50,000?

A clean npm audit and a green CI pipeline don’t answer any of those questions. The runtime probe does.

What Gets Scanned

DEPENDENCIES

CVEs across every npm package, ranked by exploitability and reachability from your routes.

EXPRESS CONFIG

Helmet coverage, CORS permissiveness, body-parser limits, cookie flags, trust-proxy settings.

RATE LIMITING

Brute-force detection on login, reset, signup, and API routes. Confirms 429s actually fire.

ERROR LEAKAGE

Stack traces, sensitive paths, env var echoes, and database errors in 500 responses.

AUTH ROUTE COVERAGE

Routes that look authenticated but skip the middleware on at least one verb (POST guarded, PATCH not).

PROTOTYPE POLLUTION SURFACE

Body parsers and merge utilities in the dep tree that accept __proto__ / constructor keys.

SSRF VECTORS

Endpoints that fetch user-supplied URLs server-side without an allowlist (image proxy, webhook tester, OG fetcher).

OPEN REDIRECT

Login or auth-callback routes that 302 to any ?next= destination — used in phishing chains.

Typical Findings

  1. Missing Helmet — no CSP, no HSTS, no frame-ancestors. Default Express ships zero security headers.
  2. CORS: * on auth routes — any origin can hit your session endpoints. When paired with credentials: true, browsers refuse, but devs sometimes “fix” by reflecting Origin blindly.
  3. Unbounded body parserexpress.json() without limit accepts a 100 MB POST and OOMs the worker.
  4. Debug routes in production/debug/env, /debug/routes, /api/__test, /_status exposed because no env gate.
  5. Express stack traces — default error handler leaks file paths, package versions, ORM queries.
  6. trust proxy not set — rate limiting keys off req.ip which is the load balancer’s IP, so every request looks like the same client.
  7. Cookie missing flags — session cookie without Secure, HttpOnly, or SameSite.
  8. JWT verified with algorithms: undefined — the dreaded alg: none accepted as valid.

How attackers find this

The recon path for a Node app is short:

  1. Hit / with curl -I. The X-Powered-By: Express header (default on, you have to disable it) confirms the stack. Server may include the Node version.
  2. Probe common Express endpoints: /api, /admin, /health, /status, /metrics, /debug, /.env. Many AI-scaffolded apps ship one or more.
  3. Trigger an error: send { to a JSON endpoint. The default error handler returns the stack trace as JSON, complete with at /Users/dev/.../routes/users.js:42:15. Now the attacker has your file structure.
  4. Identify the dep tree: a 500 page often names the failing package and version. Cross-reference Snyk or GHSA for known CVEs in that version.
  5. Replay the auth route 200×/sec. If 429s never come, brute force or credential stuffing is on the table. Most fresh Express apps have no rate limit.
  6. Try prototype-pollution payloads: POST /api/user/update {"__proto__": {"isAdmin": true}}. If the merge utility downstream is vulnerable, future requests pick up isAdmin: true from the polluted prototype.

The whole sequence is automatable and runs without raising any meaningful logs on the target side.

Fix Recipes

Hardened Express boot:

import express from 'express';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import cors from 'cors';

const app = express();

app.disable('x-powered-by');
app.set('trust proxy', 1); // adjust to your proxy depth

app.use(helmet());
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ limit: '100kb', extended: false }));

app.use(cors({
  origin: ['https://app.example.com'],
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 20,
  standardHeaders: true,
  legacyHeaders: false,
});
app.use('/auth', authLimiter);
app.use('/login', authLimiter);
app.use('/reset-password', authLimiter);

Safe error handler:

app.use((err, req, res, next) => {
  const id = crypto.randomUUID();
  console.error({ id, err });
  res.status(err.status || 500).json({
    error: 'Internal error',
    requestId: id,
    // No stack, no message, no path. The id ties to your logs.
  });
});

JWT verification — pin the algorithm:

import jwt from 'jsonwebtoken';

const payload = jwt.verify(token, PUBLIC_KEY, {
  algorithms: ['RS256'],   // Without this, alg:none and HS256-with-pubkey-as-secret both work.
  audience: 'api.example.com',
  issuer: 'https://auth.example.com',
});

Cookie flags:

res.cookie('sid', sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  maxAge: 1000 * 60 * 60 * 24 * 7,
  path: '/',
});

Environment-gated debug routes:

if (process.env.NODE_ENV !== 'production') {
  app.use('/debug', debugRouter);
}

Dependency hygiene

  • npm audit --omit=dev weekly in CI; fail the build on high or critical.
  • Pin via package-lock.json + Renovate/Dependabot. Don’t accept floating ranges in production.
  • Scan post-install scripts: npm pkg get scripts.postinstall across the tree, deny anything that fetches remote code.
  • Replace abandoned packages — anything with no commits in 18 months is a future CVE.
  • Watch for hallucinated packages — AI tools invent package names that attackers then register with malware.

What this scanner does NOT flag

  • Pure code-level issues like unsanitised template strings or insecure deserialisation that only fire with a specific payload — runtime probing covers the boundary, not every internal function.
  • Application-logic bugs (broken access control between two legitimate users) — that’s a VibeEval agent job, requires authenticated multi-user testing.
  • Issues behind feature flags or paid plans the scanner can’t reach.
  • Background-job vulnerabilities in BullMQ, Agenda, etc. — workers don’t have HTTP surface to probe from the outside.
  • Memory leaks and event-loop blocks — these are perf problems, not security; out of scope.

COMMON QUESTIONS

01
What does the scanner test?
Dependency CVEs via npm advisories, Express configuration (helmet, cors, body-parser limits), missing rate limits, exposed /admin or /debug routes, unsafe eval or child_process usage, and hardcoded secrets in code.
Q&A
02
Does it need repo access?
No. The tool probes your deployed endpoint. For code-level checks, we offer an optional repo scan — but the default flow is URL-in, findings-out.
Q&A
03
How is this different from npm audit?
npm audit checks package.json. We probe the live app — finding issues that only exist in production (misconfigured helmet, bypassed rate limits, leaked env vars in logs).
Q&A

RUN THE FULL NODE.JS AUDIT

The VibeEval agent tests your live endpoints for auth, injection, and rate-limit gaps.

START FULL SCAN