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 Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How dependabot-missing-cooldown happens in GitHub Actions/Node.js and how to fix it

The repository's `.github/dependabot.yml` had no cooldown period configured, meaning Dependabot could immediately propose updates to newly published package versions with zero time for the community to flag malware or instability. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, forcing a 7-day waiting period before new releases are surfaced as update PRs.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.

critical

How Remote Code Execution Happens in Handlebars Template Compilation and How to Fix It

CVE-2026-33937 is a critical remote code execution vulnerability in Handlebars.js that allows attackers to execute arbitrary code by passing maliciously crafted Abstract Syntax Tree (AST) objects to the compile() function. The vulnerability was patched in version 4.7.9, and we've upgraded to protect against this threat vector.