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:
-
Added
requestandreplyparameters – The Fastify framework passes these objects to every route handler.requestcontains incoming request metadata, andreplyis used to send the response. -
Rate limit guard check –
if (!rateLimit(request, reply))calls the existing rate limiter function:
- If the request exceeds the rate limit threshold,rateLimit()returnsfalse
- The handler immediately returns thereplyobject (whichrateLimit()has already populated with a 429 Too Many Requests response)
- This prevents further processing for over-limit requests -
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
buildRateLimiterfunction meant nothing if it wasn't applied to every public endpoint. One unprotected endpoint is all an attacker needs. -
The
/healthendpoint 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
/healthendpoint 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
- CWE-770: Allocation of Resources Without Limits or Throttling
- CWE-400: Uncontrolled Resource Consumption
- OWASP Denial of Service
- OWASP API Security Project – Rate Limiting
- Fastify Rate Limiting Documentation
- Semgrep Rule: Missing Rate Limiting
- GitHub PR: fix: the k-skill-proxy server defines multiple publi... in server.js