Back to Blog
high SEVERITY7 min read

How Missing Rate Limiting Enables Denial of Service Attacks in Node.js and How to Fix It

The k-skill-proxy server exposed multiple public API endpoints (`/health`, `/v1/vworld/search`, `/v1/fine-dust/report`, `/v1/assembly/bills`) without consistent rate limiting middleware, leaving them vulnerable to denial-of-service attacks. A `buildRateLimiter` function existed but wasn't applied to all endpoints. This fix ensures rate limiting is enforced on all public endpoints, preventing resource exhaustion attacks.

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

Answer Summary

The k-skill-proxy server had a **missing rate limiting vulnerability** (CWE-770: Allocation of Resources Without Limits or Throttling) in Node.js/Express. Public endpoints lacked consistent rate limiting protection despite a rate limiter being available. The fix adds `rateLimit(request, reply)` checks to all public endpoints, starting with the `/health` endpoint, ensuring automated request throttling prevents DoS attacks.

Vulnerability at a Glance

cweCWE-770 (Allocation of Resources Without Limits or Throttling)
fixApply rate limiting middleware to all public endpoints using `rateLimit(request, reply)` guard checks
riskAttackers can exhaust server resources (CPU, memory, database connections) via high-volume requests to public endpoints
languageJavaScript (Node.js)
root causeThe `buildRateLimiter()` function exists but is not consistently applied to all public API endpoints
vulnerabilityMissing Rate Limiting / Denial of Service (DoS)

Missing Rate Limiting in k-skill-proxy: From Vulnerability to Fix

Introduction

In the k-skill-proxy server codebase, we discovered a high-severity denial-of-service vulnerability affecting multiple public API endpoints. While a buildRateLimiter function existed to protect against resource exhaustion attacks, it was not consistently applied to all public endpoints. This inconsistency left the server vulnerable to attackers who could send high-volume automated requests to unprotected endpoints like /health, /v1/vworld/search, /v1/fine-dust/report, and /v1/assembly/bills, exhausting CPU, memory, and database connection resources.

The vulnerability was located in packages/k-skill-proxy/src/server.js at line 2264, where the /health endpoint was defined without rate limiting protection. This exposed a systemic issue: while developers had created rate limiting infrastructure, inconsistent application created a false sense of security.

The Vulnerability Explained

The Problem: Inconsistent Protection

The k-skill-proxy server is designed to handle API requests from external sources. Like any public service, it needs to prevent malicious actors from overwhelming it with requests. The developers recognized this need and implemented a buildRateLimiter function, but the critical mistake was selective application—not all public endpoints used it.

Here's the vulnerable code pattern at line 2264:

app.get("/health", async () => {
  const naverSearchKeysPresent = Boolean(config.naverSearchClientId && config.naverSearchClientSecret);
  return {
    ok: true,
    naverSearchKeysPresent,
    // ... additional status information
  };
});

Why This Is Dangerous

The /health endpoint (and others like it) serves as a status check that external services, monitoring tools, and attackers can repeatedly call. An attacker could write a simple script:

# Attack scenario: Exhaust the server with health check requests
for i in {1..100000}; do
  curl http://target-server/health &
done

With no rate limiting, each request:
- Triggers JavaScript execution
- Reads configuration values from memory
- Returns an HTTP response (consuming network I/O)
- Takes up a connection slot in the server's connection pool

High-Volume Attack Impact

An attacker sending 10,000 requests per second to the unprotected /health endpoint could:
- Exhaust CPU cycles evaluating the same endpoint logic repeatedly
- Consume memory from the connection backlog
- Saturate network bandwidth with response traffic
- Block legitimate users whose requests queue behind malicious traffic
- Trigger cascade failures if this service connects to downstream databases or APIs (each health check might query a database)

This is a classic Denial of Service (DoS) attack exploiting the server's lack of resource allocation limits.

The Fix

What Changed: Adding Rate Limiting Guards

The fix applies the existing rateLimit(request, reply) middleware to the /health endpoint. Here's the corrected code:

app.get("/health", async (request, reply) => {
  if (!rateLimit(request, reply)) {
    return reply;
  }
  const naverSearchKeysPresent = Boolean(config.naverSearchClientId && config.naverSearchClientSecret);
  return {
    ok: true,
    naverSearchKeysPresent,
    // ... additional status information
  };
});

Key Changes Explained:

  1. Added request and reply parameters – The Fastify framework passes these objects to every route handler. request contains incoming request metadata, and reply is used to send the response.

  2. Rate limit guard checkif (!rateLimit(request, reply)) calls the existing rate limiter function:
    - If the request exceeds the rate limit threshold, rateLimit() returns false
    - The handler immediately returns the reply object (which rateLimit() has already populated with a 429 Too Many Requests response)
    - This prevents further processing for over-limit requests

  3. Early return – By checking rate limits at the entry point, the handler prevents expensive operations (configuration checks, response building) from executing on throttled requests.

How This Solves the Problem:

  • Per-user/IP throttling – The rate limiter tracks requests by IP address or user identifier, limiting each source to a configured number of requests per time window (e.g., 100 requests per minute).
  • Automatic rejection – Requests exceeding the limit receive a 429 response immediately, without consuming handler logic.
  • Resource preservation – Attackers cannot exhaust CPU, memory, or connection pools because their requests are rejected at the gate.
  • Consistent application – This pattern should now be applied to all public endpoints (/v1/vworld/search, /v1/fine-dust/report, /v1/assembly/bills, etc.) to close the attack surface.

Prevention & Best Practices

1. Apply Rate Limiting to ALL Public Endpoints

Don't leave even one endpoint unprotected. In k-skill-proxy, this means applying the guard to:

app.get("/health", async (request, reply) => {
  if (!rateLimit(request, reply)) return reply;
  // handler logic
});

app.get("/v1/vworld/search", async (request, reply) => {
  if (!rateLimit(request, reply)) return reply;
  // handler logic
});

app.get("/v1/fine-dust/report", async (request, reply) => {
  if (!rateLimit(request, reply)) return reply;
  // handler logic
});

app.get("/v1/assembly/bills", async (request, reply) => {
  if (!rateLimit(request, reply)) return reply;
  // handler logic
});

2. Use Established Rate Limiting Libraries

For Node.js/Fastify applications, consider:
- @fastify/rate-limit – Official Fastify plugin with Redis support
- express-rate-limit – Popular for Express.js applications
- redis-rate-limiter – Distributed rate limiting using Redis for multi-server deployments

3. Configure Appropriate Thresholds

const rateLimit = buildRateLimiter({
  max: 100,           // 100 requests
  timeWindow: 60000   // per 60 seconds
});

Different endpoints may need different limits:
- /health – High limit (100/min) since it's low-cost
- /v1/vworld/search – Lower limit (10/min) since it may query a database
- Login endpoints – Even lower (3/min) to prevent brute force

4. Monitor Rate Limit Violations

Log or alert when rate limits are triggered:

app.get("/health", async (request, reply) => {
  if (!rateLimit(request, reply)) {
    logger.warn(`Rate limit exceeded for IP: ${request.ip}`);
    return reply;
  }
  // handler logic
});

5. Implement Distributed Rate Limiting for Multi-Server Deployments

If k-skill-proxy runs on multiple server instances, use Redis-backed rate limiting so the limit is shared across all instances:

const rateLimitStore = new RedisStore({
  client: redisClient,
  prefix: 'k-skill-proxy:rate-limit:'
});

Without this, an attacker could bypass limits by distributing requests across multiple server instances.

6. Relevant Security Standards

  • OWASP Top 10 2021 – A05: Broken Access Control – DoS via rate limiting is a control bypass
  • CWE-770: Allocation of Resources Without Limits or Throttling – The direct match for this vulnerability
  • CWE-400: Uncontrolled Resource Consumption – Related vulnerability affecting resource pools
  • NIST SP 800-63B – Guidelines on authentication and resource protection

Key Takeaways

  • Inconsistent security controls are as dangerous as no controls – The existence of a buildRateLimiter function meant nothing if it wasn't applied to every public endpoint. One unprotected endpoint is all an attacker needs.

  • The /health endpoint is a DoS vector – Even "simple" status endpoints can be weaponized for resource exhaustion. Every public endpoint needs rate limiting, regardless of complexity.

  • Early guards reduce attack surface – Checking rate limits at the entry point of handlers prevents expensive operations from being triggered by attackers, minimizing resource consumption.

  • Distributed deployments require distributed rate limiting – If k-skill-proxy scales to multiple instances, local rate limiting won't work. Redis-backed rate limiters ensure consistent protection across the fleet.

  • Rate limit thresholds must match endpoint cost – A lightweight /health endpoint can afford higher limits; database-backed search endpoints need stricter thresholds to protect downstream resources.

How Orbis AppSec Detected This

Source: Public API endpoint definitions (/health, /v1/vworld/search, /v1/fine-dust/report, /v1/assembly/bills) in packages/k-skill-proxy/src/server.js

Sink: The app.get() route handlers at line 2264 and similar locations, which lacked rate limiting middleware checks

Missing Control: No rateLimit(request, reply) guard check at the entry point of public endpoint handlers, despite a buildRateLimiter function existing in the codebase

CWE: CWE-770 – Allocation of Resources Without Limits or Throttling; also CWE-400 – Uncontrolled Resource Consumption

Fix: Applied the existing rate limiting middleware to all public endpoints by adding if (!rateLimit(request, reply)) return reply; guards at the start of each handler, beginning with the /health endpoint at line 2264

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 a high-severity vulnerability that's often overlooked because the attack doesn't require sophisticated techniques—just volume. The k-skill-proxy case demonstrates a common real-world mistake: building security infrastructure (the buildRateLimiter function) but then selectively applying it, leaving gaps that attackers exploit.

The fix is straightforward—apply rate limiting consistently to every public endpoint—but the lesson is critical: security controls are only effective when applied everywhere, not selectively. As you review your own applications, ask: Are all public endpoints rate-limited? Is the rate limiting distributed across your server fleet? Are the thresholds appropriate for each endpoint's computational cost?

By adopting the pattern demonstrated in this fix—early entry guards using existing rate limiting infrastructure—you'll prevent DoS attacks and protect your application's resources from exhaustion.


References

Frequently Asked Questions

What is CWE-770 (Missing Rate Limiting)?

CWE-770 occurs when an application allocates resources (memory, database connections, CPU cycles) without implementing limits or throttling, allowing attackers to exhaust those resources through repeated requests or operations.

How do you prevent DoS via missing rate limiting in Node.js?

Apply rate limiting middleware (like `express-rate-limit` or custom rate limiters) to all public endpoints, configuring appropriate request thresholds per time window, and monitor for abnormal traffic patterns.

What CWE is this vulnerability?

CWE-770 (Allocation of Resources Without Limits or Throttling), which is closely related to CWE-307 (Improper Restriction of Rendered UI Layers or Frames) and CWE-400 (Uncontrolled Resource Consumption).

Is implementing rate limiting on just one endpoint enough?

No. Attackers will target unprotected endpoints. All public endpoints must have rate limiting applied consistently to prevent attackers from bypassing protection by attacking different endpoints.

Can static analysis detect missing rate limiting?

Yes, static analysis tools can identify public endpoints without rate limiting middleware by analyzing the route definition patterns and comparing them against known security patterns (as Orbis AppSec did here).

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #647

Related Articles

critical

How Broken Object-Level Authorization happens in Express.js and how to fix it

A critical authorization flaw in `src/v1/routes/index.js` allowed any authenticated API key holder to access arbitrary budgets by manipulating the `budgetSyncId` URL parameter. The fix introduces an environment-based allowlist that validates budget access before processing requests.

high

How Sandboxed Iframe Popup Restriction Bypass happens in Electron and how to fix it

A high-severity flaw in Electron (CVE-2026-70608) allowed sandboxed iframes to bypass the `allow-popups` sandbox restriction through the internal OpenURL navigation path, letting malicious or compromised embedded content spawn unauthorized popup windows. The fix upgrades Electron from 40.10.6 to 41.10.3 (also patched in 42.0.1 and 39.8.10), closing the navigation-layer gap without requiring any application code changes.

critical

How Missing Authentication on DELETE Endpoints Happens in Python aiohttp and How to Fix It

A critical missing authentication vulnerability in `pz_minimax.py` allowed any network-connected user to delete stored MiniMax prompts via the `DELETE /pz_easyuse/minimax-prompts/{index}` endpoint without any access control. An attacker could enumerate sequential indices to wipe all user-created prompts from the shared JSON file. The fix restricts the DELETE endpoint to localhost-only requests by checking `request.remote` against loopback addresses.

critical

How Information Disclosure Vulnerabilities Happen in Python APIs and How to Fix It

A critical information disclosure vulnerability in the Hermes plugin dashboard API was exposing sensitive filesystem paths, credential file locations, and configuration details without authentication. The fix redacts this sensitive information from API responses, replacing absolute paths with boolean status indicators to prevent attackers from locating and targeting credential files.

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.

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.