Back to Blog
critical SEVERITY6 min read

How Rate Limiting Vulnerabilities Happen in Node.js OAuth Endpoints and How to Fix Them

A critical resource exhaustion vulnerability was discovered in the OAuth token endpoint at `server/routes/oauth.js`. Without rate limiting, attackers could flood the `/api/oauth/token` endpoint with requests, each triggering expensive bcrypt verification operations that would exhaust server CPU and memory. The fix implements per-IP rate limiting using `express-rate-limit` to cap requests at 20 per 15-minute window.

O
By Orbis AppSec
Published September 4, 2026Reviewed September 4, 2026

Answer Summary

This is a resource exhaustion vulnerability (CWE-770) in a Node.js Express OAuth endpoint where the `/api/oauth/token` route lacked rate limiting. Each authentication request triggers expensive bcrypt operations, allowing attackers to exhaust server resources through request flooding. The fix adds `express-rate-limit` middleware configured to allow only 20 requests per IP address within a 15-minute window, returning HTTP 429 when exceeded.

Vulnerability at a Glance

cweCWE-770 (Allocation of Resources Without Limits or Throttling)
fixAdded express-rate-limit middleware with 20 requests per 15-minute window per IP
riskDenial of Service through CPU/memory exhaustion
languageJavaScript (Node.js/Express)
root causeOAuth token endpoint accepts unlimited requests triggering expensive bcrypt operations
vulnerabilityResource Exhaustion via Missing Rate Limiting

Introduction

The /api/oauth/token endpoint in server/routes/oauth.js handles one of the most security-critical operations in any application: exchanging credentials for access tokens. However, a dangerous oversight at line 140 left this endpoint vulnerable to a resource exhaustion attack that could bring down the entire server.

The problem wasn't with the authentication logic itself—input sanitization was already in place. The issue was that every single request to this endpoint triggers an expensive bcrypt verification operation, and there was absolutely nothing stopping an attacker from sending thousands of requests per second.

// Before the fix - no rate limiting on this expensive endpoint
app.post(buildServerPath('/api/oauth/token'), async (req, res) => {
  // Each request triggers bcrypt.compare() - extremely CPU intensive

For developers building OAuth implementations or any authentication system, this vulnerability demonstrates why defense-in-depth matters: even well-validated inputs can become attack vectors when resource consumption isn't controlled.

The Vulnerability Explained

Why bcrypt Makes This Dangerous

The OAuth token endpoint uses bcrypt to verify client secrets. Bcrypt is intentionally slow—that's its security feature. Each verification operation is designed to take significant CPU time to prevent offline brute-force attacks. But this same property becomes a weapon when attackers can trigger unlimited verifications.

Here's what the vulnerable code path looked like:

app.post(buildServerPath('/api/oauth/token'), async (req, res) => {
  try {
    const platform = configCache.getPlatform() || {};
    const oauthConfig = platform.oauth || {};
    // ... bcrypt verification happens here for every request

Attack Scenario

An attacker targeting this specific endpoint could execute a straightforward denial-of-service attack:

  1. Identify the endpoint: The /api/oauth/token path is publicly accessible and follows standard OAuth conventions
  2. Craft minimal requests: Send POST requests with any grant_type, username, and password values
  3. Flood the server: Launch parallel requests from multiple IPs or a single source
  4. Exhaust resources: Each request forces the server to execute bcrypt operations, consuming CPU cycles

With just 50 concurrent requests (as tested in the regression suite), the server would be spending all its CPU time on bcrypt operations, leaving legitimate users unable to authenticate or access the application.

Real-World Impact

For this Node.js application, the impact is severe:

  • Complete service disruption: The single-threaded event loop becomes blocked by CPU-intensive bcrypt operations
  • No authentication possible: Legitimate users cannot obtain tokens
  • Cascading failures: Other routes sharing the same process become unresponsive
  • Low attack cost: The attacker needs minimal resources compared to the damage inflicted

The Fix

The fix introduces express-rate-limit middleware specifically configured for the OAuth token endpoint:

Before (Vulnerable)

app.post(buildServerPath('/api/oauth/token'), async (req, res) => {

After (Protected)

import rateLimit from 'express-rate-limit';

// Mitigates brute-force / resource-exhaustion attacks: each request to the
// token endpoint triggers an expensive bcrypt client-secret comparison, so an
// unauthenticated flood can exhaust CPU. Limit per-IP independently of any
// broader shared limiter.
const oauthTokenLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minute window
  limit: 20,                   // 20 requests per window
  standardHeaders: true,       // Return rate limit info in headers
  legacyHeaders: false,        // Disable X-RateLimit-* headers
  message: { error: 'Too many token requests from this IP, please try again later.' }
});

app.post(buildServerPath('/api/oauth/token'), oauthTokenLimiter, async (req, res) => {

Why These Specific Settings?

The configuration choices are deliberate:

Setting Value Rationale
windowMs 15 minutes Balances security with legitimate retry scenarios
limit 20 requests Allows normal OAuth flows while blocking floods
standardHeaders true Enables RateLimit-* headers per RFC draft
legacyHeaders false Avoids deprecated header format

The middleware is applied only to the token endpoint, not globally. This ensures that:
- Other routes aren't unnecessarily restricted
- The expensive bcrypt operations are specifically protected
- Rate limit state is isolated to this security-critical path

Key Security Improvement

When the rate limit is exceeded, the server immediately returns HTTP 429 (Too Many Requests) without executing the bcrypt operation. This is crucial—the expensive work is avoided entirely for excess requests, preserving CPU for legitimate traffic.

Prevention & Best Practices

1. Identify Expensive Operations

Audit your codebase for computationally expensive operations exposed to unauthenticated users:

// Red flags to look for:
bcrypt.compare()    // Password/secret verification
bcrypt.hash()       // Hashing operations
crypto.pbkdf2()     // Key derivation
sharp().resize()    // Image processing
pdf.create()        // Document generation

2. Layer Your Rate Limiting

Implement rate limiting at multiple levels:

// Global limiter for all routes
const globalLimiter = rateLimit({
  windowMs: 60 * 1000,
  limit: 100
});

// Stricter limiter for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  limit: 20
});

app.use(globalLimiter);
app.use('/api/auth', authLimiter);
app.use('/api/oauth', authLimiter);

3. Consider Additional Protections

  • Request queuing: Limit concurrent bcrypt operations
  • CAPTCHA: Add challenges after failed attempts
  • IP reputation: Block known malicious sources
  • Monitoring: Alert on unusual request patterns

4. Test Your Limits

The regression test from this PR demonstrates how to verify rate limiting:

test.each(testCases)('handles $name without resource exhaustion', async ({ name, count }) => {
  const requests = Array(count).fill(null).map(() =>
    request(app)
      .post('/api/oauth/token')
      .send({ grant_type: 'password', username: 'test', password: 'test' })
  );

  const responses = await Promise.all(requests);
  const rateLimited = responses.some(r => r.status === 429);

  expect(count === 1 || rateLimited).toBe(true);
});

Key Takeaways

  • The /api/oauth/token endpoint's bcrypt operations made it a prime DoS target — any publicly accessible endpoint with expensive operations needs rate limiting
  • Rate limiting must be applied before the expensive operation — returning 429 early prevents resource consumption, not just abuse detection
  • Per-endpoint rate limiters provide granular control — the oauthTokenLimiter is independent of any global limiter, ensuring appropriate limits for this specific threat
  • Input validation is not resource protection — this endpoint had proper input sanitization but still needed rate limiting to prevent abuse
  • The 20 requests per 15 minutes limit balances security and usability — legitimate OAuth flows rarely need more, while attacks require thousands

How Orbis AppSec Detected This

  • Source: Unauthenticated HTTP POST requests to /api/oauth/token
  • Sink: bcrypt.compare() operation triggered for every request in server/routes/oauth.js:140
  • Missing control: No rate limiting middleware protecting the expensive authentication operation
  • CWE: CWE-770 (Allocation of Resources Without Limits or Throttling)
  • Fix: Added express-rate-limit middleware configured for 20 requests per 15-minute window per IP address

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

This vulnerability in server/routes/oauth.js illustrates a critical principle: security isn't just about validating inputs—it's about controlling resource consumption. The bcrypt algorithm's intentional slowness, designed to protect against offline attacks, became a liability when exposed to unlimited online requests.

The fix is elegantly simple: a rate limiter that stops abuse before the expensive operation executes. For any developer building authentication systems, this pattern should be standard practice. Identify your expensive operations, protect them with appropriate rate limits, and test that those limits actually work under load.

References

Frequently Asked Questions

What is a resource exhaustion vulnerability?

A resource exhaustion vulnerability occurs when an application allows unlimited consumption of system resources (CPU, memory, network) without proper throttling, enabling attackers to degrade or crash the service.

How do you prevent resource exhaustion in Node.js?

Implement rate limiting middleware like express-rate-limit on expensive endpoints, use request queuing, set timeouts on operations, and consider using worker threads for CPU-intensive tasks like bcrypt.

What CWE is resource exhaustion?

CWE-770 (Allocation of Resources Without Limits or Throttling) covers vulnerabilities where applications don't properly limit resource allocation, and CWE-400 (Uncontrolled Resource Consumption) is a related parent category.

Is input validation enough to prevent resource exhaustion?

No, input validation alone doesn't prevent resource exhaustion. Even valid requests can exhaust resources when sent in high volumes. Rate limiting must be combined with input validation for comprehensive protection.

Can static analysis detect resource exhaustion vulnerabilities?

Static analysis can identify endpoints lacking rate limiting middleware and flag expensive operations like bcrypt in public routes, but dynamic testing and threat modeling are often needed to assess actual risk.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2265

Related Articles

critical

How Missing Authentication on DELETE Endpoints Happens in Node.js Express and How to Fix It

A critical authentication bypass vulnerability was discovered in the skill-cabinet server where the DELETE /api/skills/:id endpoint allowed any unauthenticated user to delete arbitrary skills from the filesystem. The fix implements loopback origin validation to ensure only requests from localhost can perform destructive operations, while also consolidating delete functionality into a single, protected endpoint.

critical

How API Key Exposure and Unsafe Process Spawning Happens in Node.js Scripts and How to Fix It

A critical security vulnerability in the `scripts/close-issues.mjs` file exposed API key patterns in documentation and used unsafe `spawnSync` calls to execute curl commands. The fix replaces dangerous process spawning with native `fetch()` API calls and removes sensitive configuration examples from documentation, eliminating both credential exposure and command injection risks.

high

How CSRF and Missing Authentication Protection Happens in Node.js Express Routes and How to Fix It

A critical vulnerability in code-server's `/mint-key` endpoint allowed unauthenticated cross-origin requests to generate or retrieve VS Code web server authentication keys. By adding the `ensureAuthenticated` middleware to the POST handler, the fix ensures only authenticated users can mint new keys, eliminating the CSRF attack vector.

critical

How JWT Signature Bypass happens in Node.js and how to fix it

A critical authentication bypass vulnerability was discovered in `backend/services/auth-state.js` where the `tokenTtlSeconds()` function used `jwt.decode()` instead of `jwt.verify()`, allowing attackers to forge JWT tokens with arbitrary claims. Because `jwt.decode()` never validates the cryptographic signature, any attacker could craft a token with a manipulated expiration time or elevated privileges and have it accepted as legitimate. The fix replaces the insecure decode call with `jwt.verify(

critical

How Unverified JWT Decoding Happens in Java and How to Fix It

A critical authentication bypass was discovered in `JwtExtractor.java` where `JWT.decode()` was used instead of a proper signature-verifying method, allowing any attacker to forge a JWT with an arbitrary username — including `admin` — and gain unauthorized access. The fix adds clear documentation establishing the trust boundary: signature validation must occur upstream, and the extracted claims are for display purposes only. This change prevents the class from being misused as an authorization g

critical

How hardcoded API key exposure happens in Node.js plugins and how to fix it

A critical hardcoded API key (`actor-studio-gpt-beta`) was discovered in the `src/plugins/llm/index.js` file of the Actor Studio application, granting anyone with source code access the ability to make unauthorized requests to the LLM service endpoints. The fix removes the default key from both the LLM class definition and the settings module, requiring the key to be explicitly configured through module settings instead.