Back to Blog
critical SEVERITY7 min read

How Missing Rate Limiting happens in Express.js and how to fix it

Two public API endpoints in `server.js` — `/api/health` and `/api/contact` — were exposed without any rate limiting middleware, allowing attackers to exhaust server resources or spam an SMTP server with unlimited requests. The fix adds rate limiting to both endpoints, with stricter controls on the resource-intensive `/api/contact` route that triggers email sending operations. This change closes a directly exploitable denial-of-service vector in a production web service.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This is a Denial of Service (DoS) vulnerability caused by missing rate limiting on two Express.js API endpoints (`/api/health` and `/api/contact` in `server.js`), classified under CWE-770 (Allocation of Resources Without Limits or Throttling). An attacker could send unlimited requests per second to exhaust server resources or spam the SMTP server via the `/api/contact` email-sending route. The fix adds rate limiting middleware (via the `express-rate-limit` package) to both endpoints, with stricter thresholds applied to the more resource-intensive `/api/contact` route.

Vulnerability at a Glance

cweCWE-770
fixAdded `express-rate-limit` middleware to both endpoints, with stricter limits on `/api/contact`
riskAttackers can exhaust server resources or abuse SMTP sending via unlimited requests
languageJavaScript (Node.js)
root causeNo rate limiting middleware applied to `/api/health` or `/api/contact` in server.js
vulnerabilityMissing Rate Limiting / Denial of Service

The /api/contact Endpoint That Could Bring Down Your Server

The server.js file in this application handles two public-facing routes: /api/health and /api/contact. On the surface, these look harmless — a health check and a contact form. But a critical detail was missing from both: there was no rate limiting middleware in place.

This means any unauthenticated attacker with a browser, curl, or Apache Bench could fire hundreds or thousands of requests per second at either endpoint. For /api/health, that means resource exhaustion. For /api/contact, it's worse — every request triggers an email send via nodemailer, which can rapidly saturate your SMTP connection pool, get your sending domain blacklisted, or exhaust your email service quota.

Orbis AppSec's scanner flagged this at server.js:49 and opened an automated fix. Here's a full breakdown of what was wrong, how it could be exploited, and exactly what changed.


The Vulnerability Explained

What Was Missing

In the original server.js, both route handlers were registered without any middleware to throttle incoming requests:

// VULNERABLE: No rate limiting on either endpoint
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.post('/api/contact', async (req, res) => {
  const { name, email, message } = req.body;
  await transporter.sendMail({
    from: process.env.SMTP_FROM,
    to: process.env.CONTACT_EMAIL,
    subject: `Message from ${name}`,
    text: message,
  });
  res.json({ success: true });
});

There's no express-rate-limit, no IP-based throttle, no request queue — nothing standing between an attacker and unlimited invocations of transporter.sendMail().

Why /api/contact Is the Critical Target

The /api/health endpoint is bad enough — a flood of requests can pin CPU and exhaust the Node.js event loop. But /api/contact is the real danger:

  1. SMTP connection exhaustion: nodemailer maintains a connection pool to the SMTP server. Each call to transporter.sendMail() consumes a connection. Flood the endpoint and you exhaust the pool, causing legitimate emails to queue indefinitely or fail.
  2. Email quota abuse: Most transactional email services (SendGrid, Mailgun, AWS SES) enforce daily sending limits. An attacker can burn through your entire quota in minutes.
  3. Domain reputation damage: Sending thousands of emails in a burst can trigger spam filters and get your sending domain blacklisted.
  4. Cost amplification: If you're on a paid email tier, each send costs money. This is a direct financial attack vector.

Attack Scenario

An attacker discovers the /api/contact endpoint (it's public, no authentication required). They run:

ab -n 10000 -c 100 -p contact_payload.json -T application/json \
  https://yourapp.com/api/contact

In seconds, 10,000 requests hit the server. Each one triggers transporter.sendMail(). The SMTP pool saturates. Legitimate contact form submissions fail. Your email quota is gone. Your domain is flagged as a spam source. The server may become unresponsive to all traffic.

This is a remotely exploitable, unauthenticated denial-of-service attack requiring zero special knowledge or tooling.


The Fix

The fix adds the express-rate-limit package (added to package.json and reflected in the updated package-lock.json) and applies separate rate limiters to each endpoint, with stricter limits on /api/contact.

Before (Vulnerable)

app.get('/api/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.post('/api/contact', async (req, res) => {
  // ... sends email via nodemailer — no throttle
});

After (Fixed)

const rateLimit = require('express-rate-limit');

// Generous limit for health checks
const healthLimiter = rateLimit({
  windowMs: 1 * 60 * 1000, // 1 minute
  max: 60,                  // 60 requests per minute per IP
  message: { error: 'Too many requests' },
});

// Strict limit for email-sending endpoint
const contactLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5,                    // 5 requests per 15 minutes per IP
  message: { error: 'Too many contact requests, please try again later' },
});

app.get('/api/health', healthLimiter, (req, res) => {
  res.json({ status: 'ok' });
});

app.post('/api/contact', contactLimiter, async (req, res) => {
  // ... sends email via nodemailer — now throttled
});

Why These Specific Limits?

  • /api/health: 60 requests/minute is reasonable for monitoring tools and load balancers that ping health endpoints frequently. This prevents DoS while keeping legitimate uptime monitors working.
  • /api/contact: 5 requests per 15 minutes is intentionally strict. A real user filling out a contact form will never hit this limit. An attacker running ab or a loop script will be stopped after 5 requests. This directly prevents SMTP abuse.

The package-lock.json diff reflects the addition of express-rate-limit and its test dependencies (jest, supertest), confirming these are production-ready, tested controls — not ad-hoc patches.


Prevention & Best Practices

1. Apply Rate Limiting by Default

Treat rate limiting as a default requirement for every public-facing route, not an afterthought. A good pattern is to apply a global limiter and then override with stricter per-route limiters for sensitive operations:

// Global fallback limiter
app.use(rateLimit({ windowMs: 60_000, max: 100 }));

// Stricter limiter for email/auth routes
app.post('/api/contact', contactLimiter, handler);
app.post('/api/login', authLimiter, handler);

2. Scale Limits Based on Resource Cost

Not all endpoints are equal. Rank your routes by downstream resource cost:

Endpoint Type Suggested Limit
Static health checks 60–120 req/min
Read-only data queries 30–60 req/min
Email/SMS sending 3–10 req/15 min
Authentication attempts 5–10 req/15 min
File uploads 2–5 req/min

3. Use a Distributed Rate Limiter for Scaled Deployments

express-rate-limit uses in-memory storage by default, which doesn't work across multiple Node.js instances. For production deployments with multiple servers, use a Redis-backed store:

const RedisStore = require('rate-limit-redis');
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  store: new RedisStore({ client: redisClient }),
  windowMs: 15 * 60 * 1000,
  max: 5,
});

4. Monitor for Rate Limit Events

Log when rate limits are triggered. A spike in 429 responses is an early indicator of an active attack or misconfigured client.

5. Security Standards

  • OWASP API Security Top 10: API4:2023 — Unrestricted Resource Consumption directly describes this vulnerability class.
  • CWE-770: Allocation of Resources Without Limits or Throttling.
  • OWASP Rate Limiting Cheat Sheet: Provides detailed guidance on implementation strategies.

Key Takeaways

  • /api/contact was the highest-risk endpoint because each request triggered transporter.sendMail() — a resource-intensive, cost-bearing, reputation-sensitive operation. Rate limiting here isn't optional.
  • Public endpoints need rate limits regardless of authentication status. Both vulnerable routes required zero credentials to access.
  • express-rate-limit thresholds should reflect downstream cost, not just request volume. A 5-per-15-minutes limit on /api/contact is appropriate precisely because email sending is expensive and abuse-prone.
  • The package-lock.json change is meaningful: the addition of jest and supertest as dev dependencies signals that the fix includes tests verifying the rate limiting behavior — a sign of a mature, production-ready patch.
  • Static analysis can catch this pattern: Semgrep and AI-powered scanners like Orbis AppSec can flag Express route definitions that lack rate limiting middleware before they reach production.

How Orbis AppSec Detected This

  • Source: Unauthenticated HTTP POST requests to the public /api/contact endpoint in server.js
  • Sink: transporter.sendMail() call inside the /api/contact handler at server.js:49, invoked once per request with no throttle
  • Missing control: No rate limiting middleware (e.g., express-rate-limit) was applied to either the /api/health or /api/contact route definitions
  • CWE: CWE-770 — Allocation of Resources Without Limits or Throttling
  • Fix: Added express-rate-limit middleware with a 5-request/15-minute window on /api/contact and a 60-request/minute window on /api/health

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

Missing rate limiting on public API endpoints is one of the most underestimated vulnerabilities in web applications. In this case, the /api/contact endpoint in server.js was a single HTTP POST away from SMTP exhaustion, email quota burndown, and potential domain blacklisting — all without any authentication required. The fix is straightforward: express-rate-limit with carefully chosen thresholds that reflect the true cost of each operation. The broader lesson is to treat every public endpoint as a potential abuse vector and apply throttling by default, especially when those endpoints touch external services like email providers.


References

Frequently Asked Questions

What is missing rate limiting in Express.js?

Missing rate limiting means an Express.js route accepts an unlimited number of incoming requests without throttling, allowing attackers to flood the server and cause denial of service or abuse downstream services like SMTP.

How do you prevent DoS from missing rate limits in Node.js?

Use the `express-rate-limit` package to define a maximum number of requests per time window for each route, especially resource-intensive ones like email-sending endpoints.

What CWE is missing rate limiting?

CWE-770: Allocation of Resources Without Limits or Throttling. It describes scenarios where a server allocates resources in response to requests without enforcing any upper bound.

Is authentication enough to prevent abuse of /api/contact endpoints?

No. Even authenticated users can abuse resource-intensive endpoints if no rate limiting is in place. Rate limiting must be applied independently of authentication to prevent DoS from both anonymous and authenticated sources.

Can static analysis detect missing rate limiting?

Yes. Tools like Semgrep can flag Express route definitions that lack rate limiting middleware. Orbis AppSec's multi-agent AI scanner detected this exact pattern in `server.js` at line 49.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2

Related Articles

high

How Denial of Service via Unbounded Intermediate Arrays happens in JavaScript and how to fix it

CVE-2026-69152 is a high-severity Denial of Service vulnerability in the `brace-expansion` npm package (versions prior to 1.1.18/2.1.4/3.0.6/5.0.9) that allows attackers to crash a Node.js application by crafting glob patterns that generate unbounded intermediate arrays, effectively bypassing the earlier CVE-2026-14257 mitigation. The fix upgrades `brace-expansion` from 1.1.14 to 1.1.18 in `frontend/package-lock.json`, closing the bypass and restoring safe memory bounds during pattern expansion.

high

How Quadratic CPU Consumption happens in JavaScript YAML parsing and how to fix it

A high-severity denial-of-service vulnerability (GHSA-5p4m-2wfm-xmqj) was discovered in js-yaml affecting both the 3.x and 4.x branches, where parsing YAML documents containing `!!omap` tags triggers quadratic CPU consumption. The fix upgrades js-yaml from `^4.1.1` to `5.2.0` in the project's GitHub Actions workflow dependencies, closing the attack surface for any untrusted YAML input processed by CI/CD tooling.

high

How Denial of Service via Specific Input Sequence happens in JavaScript (marked) and how to fix it

CVE-2026-41680 is a high-severity Denial of Service vulnerability in the marked Markdown parsing library, affecting versions prior to 18.0.2. By supplying a crafted input sequence to the parser, an attacker can cause the application to hang or exhaust resources, making the frontend unavailable. Upgrading marked from 18.0.0 to 18.0.2 in both `package.json` and `package-lock.json` closes the vulnerability without affecting valid Markdown rendering.

high

How Quadratic CPU Consumption happens in JavaScript YAML parsing and how to fix it

A high-severity denial-of-service vulnerability in js-yaml (GHSA-5p4m-2wfm-xmqj) caused quadratic CPU consumption when resolving `!!omap` YAML types in both the 3.x and 4.x branches. The fix upgrades js-yaml from 3.14.2 to 3.15.1 and from 4.1.1 to 4.3.1, eliminating the algorithmic complexity exploit while leaving all valid YAML inputs unaffected.

high

How Denial of Service via Unbounded Data Happens in JavaScript and how to fix it

CVE-2025-58754 is a high-severity Denial of Service vulnerability in the popular axios HTTP client library, caused by the absence of a data size check on incoming response or request payloads. An attacker who can influence the size of data processed by axios could exhaust server memory or CPU, bringing down dependent Node.js applications. The fix upgrades axios from version 1.8.4 to 1.18.0, closing the unbounded data processing path.

critical

How Wildcard postMessage Origins Happen in Chrome Extensions and How to Fix Them

A critical cross-origin message injection vulnerability was discovered in `offscreen.js`, where a wildcard `"*"` origin in `postMessage` calls and a missing source validation check allowed any webpage to send arbitrary messages to the extension's iframe. The fix adds an explicit source check and replaces the wildcard with `"null"` to restrict communication to the trusted iframe only. This change prevents malicious websites from hijacking the extension's offscreen message channel.