Back to Blog
critical SEVERITY9 min read

How Unbounded JSON Body Parsing happens in Cloudflare Workers and how to fix it

A critical denial-of-service vulnerability in `_workers.js` allowed attackers to send arbitrarily large or deeply nested JSON payloads to the `/api/log-speed` POST endpoint, causing the Cloudflare Worker to exhaust CPU and memory during parsing. The fix adds a Content-Length header check before calling `await request.json()`, rejecting payloads over 10 KB with an HTTP 413 response. This prevents resource exhaustion attacks while preserving all legitimate functionality.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This vulnerability is an unbounded request body (CWE-400: Uncontrolled Resource Consumption) in a Cloudflare Worker written in JavaScript. The `/api/log-speed` POST endpoint at line 568 of `_workers.js` called `await request.json()` without checking the `Content-Length` header or enforcing a maximum body size, allowing an attacker to send a multi-megabyte or deeply nested JSON payload to exhaust worker CPU and memory. The fix reads the `Content-Length` header before parsing and returns HTTP 413 if it exceeds 10,240 bytes, preventing the expensive JSON parse from ever running on oversized input.

Vulnerability at a Glance

cweCWE-400
fixRead `Content-Length` before parsing; return HTTP 413 and skip JSON parsing when the declared size exceeds 10,240 bytes
riskAn unauthenticated attacker can crash or throttle the worker by sending a large or deeply nested JSON payload
languageJavaScript (Cloudflare Workers / Service Worker API)
root cause`await request.json()` is called at line 569 without first validating the `Content-Length` header or enforcing a body-size limit
vulnerabilityUncontrolled Resource Consumption (DoS via oversized JSON body)

How Unbounded JSON Body Parsing Happens in Cloudflare Workers and How to Fix It


Vulnerability at a Glance

Field Detail
Vulnerability Uncontrolled Resource Consumption (DoS via oversized JSON body)
CWE CWE-400
Language JavaScript (Cloudflare Workers)
Risk Unauthenticated attacker can exhaust worker CPU/memory
Root Cause await request.json() called without a body-size guard
Fix Reject requests with Content-Length > 10240 before parsing

Summary

A critical denial-of-service vulnerability in _workers.js allowed attackers to send arbitrarily large or deeply nested JSON payloads to the /api/log-speed POST endpoint, causing the Cloudflare Worker to exhaust CPU and memory during parsing. The fix adds a Content-Length header check before calling await request.json(), rejecting payloads over 10 KB with an HTTP 413 response. This prevents resource exhaustion attacks while preserving all legitimate functionality.


Introduction

The _workers.js file serves as the main request router for a Cloudflare Worker deployment, handling everything from CORS preflight to speed-test history logging. One of its POST endpoints—/api/log-speed—accepts JSON payloads describing network speed measurements. At line 568, the handler jumped straight into parsing the request body:

const body = await request.json();

No size check. No header validation. No limit of any kind.

This single line meant that anyone who could reach the endpoint could force the worker to allocate memory and burn CPU cycles proportional to whatever they sent—whether that was a 50 MB blob of random bytes or a JSON object nested ten thousand levels deep. For developers building similar Worker-based APIs, this is an easy pattern to miss because the Fetch API's request.json() looks deceptively safe—it's a standard method, it handles errors gracefully, and it returns a clean JavaScript object. What it does not do is protect you from what's inside the request before it starts parsing.


The Vulnerability Explained

What the vulnerable code looked like

Before the fix, the /api/log-speed handler in _workers.js looked like this (starting at line 569):

// ==================== 测速历史记录 (POST) ====================
if (url.pathname === '/api/log-speed' && request.method === 'POST') {
  try {
    const body = await request.json();   // ← vulnerable line
    const record = {
      timestamp: Date.now(),
      // ... rest of record construction
    };
  }
}

The problem is that request.json() in the Fetch API (used natively in Cloudflare Workers) will happily consume the entire request body stream before attempting to parse it. There is no built-in size cap. If the body is 100 MB, the runtime will buffer 100 MB. If the JSON is valid but nested 100,000 levels deep, the recursive parser will happily walk every level—consuming stack and heap as it goes.

How an attacker exploits this

An attacker doesn't need credentials, a session cookie, or any prior knowledge of the application. They only need the endpoint URL. A simple attack looks like this:

# Send a 50 MB payload of valid JSON
python3 -c "
import json, sys
payload = {'data': 'A' * (50 * 1024 * 1024)}
sys.stdout.write(json.dumps(payload))
" | curl -s -X POST https://your-worker.workers.dev/api/log-speed \
     -H 'Content-Type: application/json' \
     --data-binary @-

Or, more insidiously, a deeply nested object that is small in bytes but catastrophic to parse:

# Deeply nested JSON: small payload, huge parse cost
nested = "x"
for _ in range(100_000):
    nested = f'{{"a":{nested}}}'

# Result: ~700 KB of text but requires 100,000 recursive parse steps

In Cloudflare Workers, CPU time is metered per request. A single such request can consume the worker's entire CPU budget, causing it to return a 503 to all concurrent legitimate users. Repeated at scale, this becomes a sustained denial-of-service without ever needing to send high-bandwidth traffic.

Real-world impact for this application

The /api/log-speed endpoint is designed to log speed-test results—likely from an end-user browser or mobile client. Because it's a public-facing POST endpoint (no authentication check is visible in the diff context), it is reachable by anyone. An attacker who discovers this endpoint can:

  1. Exhaust the worker's CPU quota, causing 503 errors for all users of the application.
  2. Trigger Cloudflare's resource limits, potentially resulting in the worker being suspended.
  3. Amplify the attack by running concurrent requests from multiple IPs, since each request independently triggers the expensive parse.

The Fix

What changed

The fix adds seven lines immediately before the await request.json() call, at line 569 of _workers.js:

Before:

if (url.pathname === '/api/log-speed' && request.method === 'POST') {
  try {
    const body = await request.json();
    const record = {
      timestamp: Date.now(),

After:

if (url.pathname === '/api/log-speed' && request.method === 'POST') {
  try {
    const contentLength = parseInt(request.headers.get('content-length') || '0');
    if (contentLength > 10240) {
      return new Response(JSON.stringify({ ok: false, error: 'payload too large' }), {
        status: 413,
        headers: { 'content-type': 'application/json', ...CORS_HEADERS, ...SECURITY_HEADERS }
      });
    }
    const body = await request.json();
    const record = {
      timestamp: Date.now(),

Why this fix works

  1. Read before consume: request.headers.get('content-length') reads a single HTTP header—a near-zero-cost operation—before the body stream is ever touched.

  2. Hard numeric limit: The 10,240-byte (10 KB) threshold is generous for a speed-test log record (which typically contains a few numeric fields and a timestamp) while being tiny compared to any meaningful attack payload.

  3. Early return with proper status: Returning HTTP 413 Payload Too Large is the semantically correct response. It tells well-behaved clients they sent too much data, without burning any additional CPU on parsing.

  4. Consistent headers: The response spreads CORS_HEADERS and SECURITY_HEADERS—the same header sets used elsewhere in the worker—so the error response is indistinguishable in structure from normal responses, avoiding information leakage about the internal routing logic.

  5. parseInt with fallback: Using parseInt(...|| '0') gracefully handles missing or malformed Content-Length headers. A missing header evaluates to '0', which is ≤ 10240 and allows the request through—appropriate because some legitimate HTTP clients omit Content-Length when using chunked transfer encoding. For production hardening, you may also want to add a streaming byte-counter for those cases (see Best Practices below).


Prevention & Best Practices

1. Always validate Content-Length before parsing bodies

Make this a standard pattern for every POST/PUT/PATCH endpoint in your Workers:

function checkBodySize(request, maxBytes = 10240) {
  const contentLength = parseInt(request.headers.get('content-length') || '0');
  if (contentLength > maxBytes) {
    return new Response(JSON.stringify({ ok: false, error: 'payload too large' }), {
      status: 413,
      headers: { 'content-type': 'application/json' }
    });
  }
  return null; // ok to proceed
}

// Usage
const sizeError = checkBodySize(request);
if (sizeError) return sizeError;
const body = await request.json();

2. Add a streaming byte counter for chunked requests

Content-Length can be absent (chunked transfer encoding) or spoofed by a malicious client. For defense in depth, count bytes as you stream:

async function readBodyWithLimit(request, maxBytes = 10240) {
  const reader = request.body.getReader();
  let received = 0;
  const chunks = [];

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    received += value.length;
    if (received > maxBytes) {
      reader.cancel();
      throw new Error('payload too large');
    }
    chunks.push(value);
  }

  const combined = new Uint8Array(received);
  let offset = 0;
  for (const chunk of chunks) {
    combined.set(chunk, offset);
    offset += chunk.length;
  }
  return JSON.parse(new TextDecoder().decode(combined));
}

3. Validate JSON schema after parsing

Even a small, well-formed JSON payload can contain unexpected fields. Use a schema validator like zod or ajv to reject structurally invalid bodies early:

import { z } from 'zod';

const SpeedRecordSchema = z.object({
  download: z.number().min(0).max(10_000),
  upload: z.number().min(0).max(10_000),
  ping: z.number().min(0).max(10_000),
});

const parsed = SpeedRecordSchema.safeParse(body);
if (!parsed.success) {
  return new Response(JSON.stringify({ ok: false, error: 'invalid payload' }), { status: 400 });
}

4. Apply rate limiting at the Cloudflare level

Cloudflare Workers support Rate Limiting rules and the Rate Limiter API. Even with a body-size check in place, rate limiting provides a second layer of defense against high-frequency attacks.

5. Security standards

  • OWASP API Security Top 10 – API4:2023: Unrestricted Resource Consumption — directly describes this class of vulnerability.
  • CWE-400: Uncontrolled Resource Consumption.
  • CWE-770: Allocation of Resources Without Limits or Throttling.

Key Takeaways

  • await request.json() in Cloudflare Workers has no built-in size limit. Any endpoint that calls it without a preceding size check is potentially vulnerable to resource exhaustion.
  • The Content-Length header check must come before the body is consumed, not after. Reading the header is essentially free; parsing a 50 MB JSON body is not.
  • A 10 KB limit is appropriate for the /api/log-speed endpoint because a legitimate speed-test record contains only a handful of numeric fields. Matching your limit to your actual data model makes the guard both effective and non-disruptive.
  • HTTP 413 is the correct status code for oversized payloads—using it correctly communicates intent to clients and monitoring systems alike.
  • Content-Length alone is not sufficient for a complete defense; pairing it with a streaming byte counter handles chunked-encoding edge cases that a header-only check misses.

How Orbis AppSec Detected This

  • Source: Inbound HTTP POST request body to the /api/log-speed endpoint in _workers.js
  • Sink: await request.json() at line 569, which unconditionally consumes and parses the full request body stream
  • Missing control: No Content-Length header validation and no maximum body size enforcement before the expensive parse operation
  • CWE: CWE-400 – Uncontrolled Resource Consumption
  • Fix: Read Content-Length before parsing and return HTTP 413 if it exceeds 10,240 bytes, preventing the JSON parser from ever running on oversized input

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

Unbounded request body parsing is one of those vulnerabilities that looks harmless in isolation—request.json() is a standard, well-documented API call—but becomes a serious availability risk the moment an endpoint is exposed to the public internet. In _workers.js, a single missing size check on the /api/log-speed endpoint was enough to give any unauthenticated attacker the ability to exhaust the Cloudflare Worker's CPU budget on demand.

The fix is elegant in its simplicity: seven lines that read one header and return one response. The key lesson for anyone building Worker-based APIs is to treat body parsing as a resource-consuming operation that must be gated, not a free utility call. Pair Content-Length checks with streaming byte counters, schema validation, and platform-level rate limiting, and you'll have a robust defense against this entire class of attack.


References

Frequently Asked Questions

What is uncontrolled resource consumption in a web API?

It occurs when an endpoint processes user-supplied data—such as a JSON body—without bounding its size or complexity, letting an attacker exhaust server memory or CPU by sending oversized or deeply nested input.

How do you prevent oversized JSON body attacks in Cloudflare Workers?

Check the `Content-Length` request header before calling `request.json()` and return HTTP 413 if the declared size exceeds your limit. For extra safety, also wrap the parse in a try/catch and consider streaming with a byte counter.

What CWE is unbounded JSON parsing?

CWE-400 – Uncontrolled Resource Consumption, sometimes also categorized under CWE-770 (Allocation of Resources Without Limits or Throttling).

Is rate limiting enough to prevent this type of attack?

Rate limiting reduces frequency but does not prevent a single oversized request from consuming excessive resources. A body-size check is a necessary complement.

Can static analysis detect missing Content-Length checks?

Yes. Tools like Semgrep can flag patterns where `request.json()` or `request.body` is consumed without a preceding size or header validation, as demonstrated by the Orbis AppSec scanner that caught this issue.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2

Related Articles

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A high-severity misconfiguration in `.github/dependabot.yml` left this Node.js library without a cooldown period, meaning Dependabot would immediately propose updates to newly published packages — including potentially malicious or unstable ones. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` package ecosystem entries, introducing a mandatory 7-day waiting period before any new package version is surfaced as an update candidate.

critical

How CSRF Protection Failures Happen in FastAPI and How to Fix Them

A critical CORS misconfiguration in `backend/main.py` allowed cookies to be sent alongside wildcard-origin requests, violating the CORS specification and opening the door to cross-site request forgery attacks. The fix conditionally disables `allow_credentials` when the allowed origins list contains a wildcard, bringing the configuration into compliance with browser security rules. This change closes a subtle but dangerous gap that could have let attackers on sibling subdomains forge authenticate

critical

How Missing Rate Limiting Happens in Node.js SSE Handlers and How to Fix It

A critical missing rate-limiting control in `src/sse/handlers/chat.js` allowed any caller to flood the SSE chat endpoint with unlimited requests, risking server resource exhaustion, denial of service, and runaway AI provider API costs. The fix introduces a per-IP sliding-window rate limiter that caps requests at 60 per minute and returns HTTP 429 on violations. Because the endpoint was publicly reachable and only validated API keys — not request frequency — exploitation required nothing more tha

medium

How Denial of Service via Catastrophic Backtracking happens in Node.js and how to fix it

CVE-2026-4867 is a Denial of Service vulnerability in path-to-regexp 0.1.12 where malformed URL parameters can trigger catastrophic backtracking in the library's regular expression engine, allowing an attacker to hang or crash a Node.js application with a single crafted request. The fix upgrades path-to-regexp to version 0.1.13, which patches the vulnerable regex patterns. This change was applied via a package-level override to ensure the patched version is used throughout the entire dependency

high

How Denial of Service via Exponential-Time Complexity happens in Node.js and how to fix it

CVE-2026-13149 is a high-severity Denial of Service vulnerability in the `brace-expansion` npm package, where crafted input strings trigger exponential-time processing that can freeze or crash a Node.js application. The fix upgrades `brace-expansion` from `2.0.2` to `2.1.4` and `minimatch` from `5.1.6` to `5.1.9`, along with npm `overrides` to ensure the patched versions are used throughout the entire dependency tree.

critical

How Unrestricted File Upload happens in Node.js/Express and how to fix it

A critical unrestricted file upload vulnerability was discovered in `mainsystem/routes/admin/profile.js`, where the avatar upload endpoint accepted any file type without validation. An authenticated attacker could upload a malicious server-side script to a web-accessible directory and execute arbitrary code on the server. The fix adds MIME type filtering, an allowlist of safe image formats, and a 2 MB file size limit to the multer middleware.