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
•Technically reviewed by Anupam Mediratta•Published September 7, 2026•Reviewed 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 check – if (!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.


Prevention and further reading

Frequently Asked Questions

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.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #647

Related Articles

critical

Discord Bot Auto-Role CWE-269: Privilege Escalation via role.position

A Discord.js bot's auto-role feature allowed privilege escalation where any user with ManageRoles permission could add Administrator roles to automatic assignment lists, regardless of their own role hierarchy. The vulnerability existed because role.position validation checked only the bot's permissions, not the invoking user's role hierarchy relative to the target role.

high

CVE-2026-54290: hono cors() Reflects Any Origin With Credentials

The `hono` CORS middleware, as resolved in this service at 4.12.8, reflected the caller's `Origin` header back in `Access-Control-Allow-Origin` while also emitting `Access-Control-Allow-Credentials: true` whenever the `origin` option was left at its `'*'` default. That combination makes any website a trusted origin for credentialed cross-origin reads. The dependency range was raised from `^4.7.1` to `^4.13.5`, moving the installed copy from 4.12.8 to 4.13.5.

critical

POST /api/generate Lacks Authentication, Allowing Unauthenticated

A resume generation endpoint in a Node.js backend accepted requests from any caller with network access, allowing attackers to consume OpenAI API quota without restriction. The vulnerability stemmed from missing authentication middleware on a cost-bearing endpoint. The fix adds mandatory API key validation via HTTP headers before processing any generation requests.

critical

SchedulePush.disableReminder Missing Authorization Check in push.js

The `disableReminder` method in the `SchedulePush` class allowed any user to disable push notification reminders for arbitrary user IDs by manipulating the `e.user_id` event parameter. The fix adds a `checkFriend()` authorization gate that verifies the requesting user has a valid friendship relationship with the bot before modifying subscription state.

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

CVE-2026-4800: lodash Template Imports Allow Code Execution

CVE-2026-4800 affects lodash's `_.template()` templating API, where untrusted input reaching the `imports` option can lead to arbitrary code execution. The fix was shipped as a dependency upgrade from lodash 4.17.21 to 4.18.1 in the project's lockfile, though the PR itself notes it was never verified against the actual code paths in this repository.