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:
- SMTP connection exhaustion:
nodemailermaintains a connection pool to the SMTP server. Each call totransporter.sendMail()consumes a connection. Flood the endpoint and you exhaust the pool, causing legitimate emails to queue indefinitely or fail. - 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.
- Domain reputation damage: Sending thousands of emails in a burst can trigger spam filters and get your sending domain blacklisted.
- 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 runningabor 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/contactwas the highest-risk endpoint because each request triggeredtransporter.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-limitthresholds should reflect downstream cost, not just request volume. A 5-per-15-minutes limit on/api/contactis appropriate precisely because email sending is expensive and abuse-prone.- The
package-lock.jsonchange is meaningful: the addition ofjestandsupertestas 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/contactendpoint inserver.js - Sink:
transporter.sendMail()call inside the/api/contacthandler atserver.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/healthor/api/contactroute definitions - CWE: CWE-770 — Allocation of Resources Without Limits or Throttling
- Fix: Added
express-rate-limitmiddleware with a 5-request/15-minute window on/api/contactand 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
- CWE-770: Allocation of Resources Without Limits or Throttling
- OWASP API Security Top 10 — API4:2023 Unrestricted Resource Consumption
- OWASP Denial of Service Cheat Sheet
- express-rate-limit Official Documentation
- Semgrep rules for missing rate limiting
- fix: the application exposes two public api endpoint... in server.js