GitHub - cr0hn/slowjson

28 min read Original article ↗

Full disclosure. This research is published openly with a reproducible Docker testbed and the complete attack CLI. The exposure described here stems from default configuration, not a hidden bug, and the mitigation is a one-line config change available today.


Table of Contents


What is this?

TL;DR: 64 simultaneous requests, each sending one byte per second from a commodity host, can make an unhardened PHP/Laravel, ASP.NET Core, or Java Spring worker pool unresponsive in under 2 minutes. The testbed covers 41 targets (32 application frameworks plus 9 infrastructure components such as proxies, WAFs, and API gateways); 37 of them (90%) are exposed under default configuration. The fix is a one-line config change you probably haven't made.

Slow JSON Stream is a low-bandwidth denial-of-service attack against any HTTP server that accepts application/json request bodies.

The attacker opens a connection, sends a syntactically valid but never-completed JSON prefix ({"items":[{"a":1},) at one byte per second, and holds it open. The server's JSON parser blocks waiting for the closing token that never arrives. Each stalled connection occupies a worker thread, goroutine, or event-loop slot. Open 64 of them simultaneously and the server's worker pool is exhausted: legitimate requests queue up, time out, and fail.

The total bandwidth required is < 1 kbps. No custom tooling or specialized knowledge is required beyond running the testbed in this repo.

What makes this different from Slowloris (the classic header-drip attack, well-mitigated since 2011): Slowloris never sends headers. Every reverse proxy closes that in 10 to 60 s. Slow JSON Stream attacks the body reader, a layer that 30 of 32 application frameworks leave completely unprotected. The body-read timeout equivalent of client_header_timeout either doesn't exist or is set to minutes in every tested framework's default config.

This repository contains the slowjson attack CLI, a Docker-based testbed of all 41 targets (32 application frameworks plus 9 infrastructure components), and the full research paper.


Prior art: what already exists and how this is different

We reviewed the full range of related attacks before publishing. The short answer: the specific combination used here has not been described before.

The slow HTTP family

Slowloris (2009) attacks the header phase. It never sends the final \r\n that terminates HTTP headers. That attack is well-mitigated by client_header_timeout defaults hardened for 15 years; it has nothing to do with the body.

R.U.D.Y. / Slow POST (2011) is the closest predecessor. The attacker declares a large Content-Length and drips the body at 1 byte per 10 seconds. Three key differences from Slow JSON Stream:

  1. R.U.D.Y. uses Content-Length; Slow JSON Stream uses Transfer-Encoding: chunked. With chunked encoding there is no declared body size, so the server must wait for the chunk terminator 0\r\n\r\n that never arrives.
  2. R.U.D.Y. sends arbitrary bytes; Slow JSON Stream sends a syntactically valid JSON prefix. The JSON parser cannot reject it: every byte delivered so far is a legal prefix of a complete document. The connection is blocked at two layers simultaneously: HTTP (waiting for the final chunk) and the application JSON parser (waiting for the closing }/]).
  3. WAF evasion: R.U.D.Y. detection typically inspects the declared Content-Length. Slow JSON Stream has no declared size and the traffic is indistinguishable from a legitimately slow JSON upload.

Slow Read (2012) is an outbound attack: the attacker advertises a tiny TCP receive window, blocking the server's send(). Slow JSON Stream is inbound.

Chunked encoding bugs (CVE-2024-22019, Node.js 2024)

This CVE also uses Transfer-Encoding: chunked as the attack vector, but exploits chunk extension fields (optional metadata bytes appended after the chunk size). A single connection can saturate the server. Slow JSON Stream sends well-formed chunk boundaries with a short body and the blocking is in the JSON parser, not the HTTP framing layer. Mitigating CVE-2024-22019 does not mitigate Slow JSON Stream.

JSON parser DoS via complexity (CVE-2021-42717, CVE-2023-5072, CVE-2024-21907…)

These attacks send a complete JSON body at full line rate, crafted to trigger worst-case algorithmic behaviour: extreme nesting depth, exponential key expansion, or huge numerals. They are volumetric complexity attacks. Slow JSON Stream carries no adversarial structure and works against any conformant JSON parser. The attack exploits blocking I/O semantics, not parser internals.

Summary table

Attack Delivery mechanism Target layer Payload Notes
Slowloris Incomplete headers HTTP headers n/a Fully mitigated since ~2011
R.U.D.Y. Content-Length + drip HTTP body Arbitrary bytes No JSON semantics; WAF-detectable
Slow Read Tiny TCP window TCP socket n/a Outbound attack
CVE-2024-22019 Chunk extension bytes HTTP framing Arbitrary Single-connection; different layer
JSON complexity DoS Full-rate JSON parser Structured payload Complete body; algorithmic, not timing
Slow JSON Stream Chunked TE + drip HTTP body + JSON parser Valid JSON prefix This work, dual-layer blocking

The key insight: client_header_timeout is set to 10 to 60 s in every major reverse proxy. A body-read timeout equivalent (client_body_timeout, MinRequestBodyDataRate, ReadTimeout) is absent or set to minutes in the default configuration of 30 out of 32 frameworks tested. R.U.D.Y. mitigations (body size limits, Content-Length inspection) do not apply to chunked requests.


How the attack works

Slow JSON Stream — Attack Flow

The payload stays syntactically valid throughout. The parser cannot reject it: every byte delivered so far is a legal prefix of a complete JSON document.


Experimental methodology

This research uses a systematic, reproducible approach to evaluate 41 targets (32 application frameworks plus 9 infrastructure components) across 320 experiment cells (~960 runs). The methodology combines controlled attack execution with impact measurement to classify exposure under default configuration.

Testbed design

Infrastructure:

  • 41 target frameworks containerized with Docker Compose
  • 32 application frameworks (PHP, Java, .NET, Node.js, Python, Go, Rust, Ruby, Elixir)
  • 9 infrastructure components (nginx, Caddy, HAProxy, Traefik, Kong, Envoy, ModSecurity, Coraza)
  • Isolated network environment with controlled resource allocation
  • Standardized backend (stub-backend) for proxy/gateway testing

Attack parameters:

  • Payload delivery rate: 1 byte per second (< 1 kbps total bandwidth)
  • Connection concurrency: 64 simultaneous connections per test
  • Attack duration: 90 seconds per experiment
  • Payload shapes: 4 variants (Array, Nested objects, Flat object, Large string)
  • Repetition: 3 runs per configuration for statistical reliability

Measurement framework

Primary metrics collected:

  1. RSS memory growth

    • Measured via docker stats every 2 seconds during attack
    • Calculated as linear slope (MB/min) over attack duration
    • Threshold: ≥ 5 MB/min indicates unbounded memory accumulation
  2. Probe latency degradation

    • Independent HTTP health checks (/healthz) every 5 seconds
    • Compare attack p99 latency vs baseline p99
    • Threshold: ≥ 5× baseline indicates service degradation
  3. Error rate monitoring

    • Track probe request failures during attack
    • Threshold: ≥ 10% error rate indicates service unavailability
  4. Connection lifecycle

    • Monitor attack connection survival over 90-second window
    • Track early termination vs indefinite holding
    • Timeout detection: Connections held for full 90s indicate no body timeout

Vulnerability classification rules

Each experimental cell is evaluated against four evidence-based rules:

Rule Condition Signal Implication
Rule 1 Probe p99 ≥ 5× baseline Service latency degradation Immediate user-visible impact
Rule 2 Probe error rate ≥ 10% Service availability loss Requests failing/timing out
Rule 3 RSS slope ≥ 5 MB/min Unbounded memory growth Resource exhaustion over time
Rule 4 Connections held ≥ 90s No body read timeout Worker pool vulnerable at scale

Verdict logic: Framework classified as VULNERABLE if any rule triggers. RESISTANT only if no rule triggers across all payload shapes and repetitions.

Tier classification system

Frameworks are categorized into 4 tiers based on attack requirements and impact severity:

Tier 1: immediate observable impact

  • Criteria: Rules 1, 2, or 3 triggered at C=64 connections
  • Real-world threat: a commodity host causes measurable degradation
  • Timeline: Impact visible within 90 seconds
  • Examples: PHP/Laravel (258 MB/min RSS growth), .NET/Kestrel (120 MB/min)

Tier 2: no body timeout (latent vulnerability)

  • Criteria: Rule 4 only; connections held indefinitely but no immediate degradation
  • Real-world threat: Exploitable once concurrency reaches deployment ceiling
  • Timeline: Worker pool exhaustion at C=100 to 500 (production typical)
  • Examples: Node.js async frameworks, Go with default timeouts, Rust async

Tier 3: hard service failure

  • Criteria: Rules 1+2; high error rate with service unavailability
  • Real-world threat: Service completely down, no partial availability
  • Timeline: Immediate total failure at C=64
  • Examples: Python/Flask (94% error rate), Ruby/Rails (100% error rate)

Tier 4: effective protection

  • Criteria: No rules triggered; built-in resistance mechanisms
  • Real-world threat: Attack fails due to architectural protections
  • Protection types: Buffer limits, streaming parsers, defensive timeouts
  • Examples: go-fiber (4KB buffer ceiling), python-fastapi-streaming (ijson)

Attack execution protocol

Single-cell experiment:

  1. Baseline measurement: 30-second probe collection with no attack traffic
  2. Attack launch: 64 connections opened, JSON drip begins (1 B/s per connection)
  3. Concurrent monitoring: Probe requests + Docker stats collection
  4. Attack conclusion: Connections closed after 90 seconds
  5. Data processing: Metric aggregation and rule evaluation

Matrix execution:

  • Total experimental cells: 41 targets × 4 payloads × 3 repetitions = 492 base cells
  • Extended analysis: 277 additional cells with varied concurrency/duration
  • Execution time: ~12 hours for full matrix on single machine
  • Resource requirements: 2GB+ RAM (PHP frameworks), Docker with compose plugin

Quality assurance

Reproducibility measures:

  • Containerized isolation: Each framework runs in standardized Docker environment
  • Resource constraints: Memory/CPU limits prevent interference between experiments
  • Network simulation: Controlled bandwidth and latency conditions
  • Statistical validation: 3-run average with outlier detection

Bias mitigation:

  • Framework neutrality: Default configurations used for all targets
  • Payload variety: 4 distinct JSON shapes prevent structure-specific bias
  • Independent measurement: Probe requests isolated from attack connections
  • Double-blind analysis: Automated verdict classification removes human judgment

Data analysis pipeline

Automated processing:

  1. Raw data collection: Docker stats + probe logs → JSON artifacts
  2. Metric computation: RSS slopes, p99 ratios, error rates → quantitative measures
  3. Rule application: Threshold comparison → binary vulnerability classification
  4. Tier assignment: Evidence pattern matching → 4-tier severity classification
  5. CVSS scoring: Deployment-aware scoring → framework-specific risk assessment

Statistical rigor:

  • Confidence intervals: 95% CI reported for key metrics
  • Significance testing: t-tests for baseline vs attack comparisons
  • Effect size: Cohen's d for practical significance assessment
  • Outlier handling: IQR-based filtering with manual verification

This methodology ensures that results are reproducible, statistically valid, and practically relevant for real-world security assessment.


Experimental results

Summary

Metric Value
Total targets tested 41 (32 application frameworks + 9 proxies/WAFs/gateways)
Targets exposed by default (any tier) 37 / 41 (90%)
Targets with observed degradation at C=64 6 / 41 (5 frameworks + Kong CE)
Targets with no body timeout (exposed at production concurrency) 29 / 41
Targets with immediate saturation at C=64 2 / 41
Targets with effective protection 4 / 41

Note on methodology: The experiment uses two categories of evidence. Observed impact means measurable RSS memory growth (≥ 5 MB/min) or probe latency degradation (≥ 5× baseline) at C=64 connections, a direct hardware-level signal achievable from a commodity host. No body timeout means the server kept 64 connections open for the full 90 s with zero degradation at that concurrency level but has no mechanism to ever close them. Goroutine and event-loop slots are occupied indefinitely, and the worker pool collapses once concurrency reaches the deployment ceiling (typically C=100 to 500 in production). Both categories are exposed; only the required investment differs.


Security impact overview

Vulnerability severity by framework

Attack effort required CVSS score distribution

Ecosystem vulnerability rates

Key takeaways:

  • PHP/Laravel: worker pool unresponsive in under 2 minutes at 64 connections (258 MB/min RSS growth)
  • 6 targets: observed degradation at C=64 (5 frameworks plus Kong CE)
  • 29 targets: no body timeout, exposed once concurrency reaches the deployment ceiling (256+)
  • 2 targets: immediate saturation at C=64 (python-flask, ruby-rails)
  • Only 4 resistant out of 41 tested

CVSS scoring methodology

This section details the final step of our experimental methodology: translating technical findings into standardized risk scores.

Why framework-specific CVSS scores matter: Unlike generic vulnerability disclosure where one score fits all, the real-world impact of Slow JSON Stream varies based on deployment patterns. A Node.js microservice failing cascades through dependent services (Changed Scope), while a standalone PHP app only affects itself (Unchanged Scope). This research provides deployment-aware CVSS scoring, the first of its kind for this attack class.

Base vector analysis

All vulnerable frameworks share the same base attack characteristics:

CVSS 3.1 Metric Value Reasoning
Attack Vector (AV) Network (N) JSON API endpoints are network-accessible
Attack Complexity (AC) Low (L) Standard HTTP chunked transfer; no special conditions
Privileges Required (PR) None (N) ¹ Most REST JSON APIs accept unauthenticated POST
User Interaction (UI) None (N) Fully automated, no victim action needed
Confidentiality Impact (C) None (N) No data exfiltrated
Integrity Impact (I) None (N) No data modified
Availability Impact (A) High (H) Service fully unavailable to legitimate users

¹ Authentication assumption: We assume unauthenticated JSON endpoints (PR:N) as the common case for REST APIs, webhooks, and public integrations. For authenticated-only endpoints, use PR:L and subtract ~2 CVSS points.

The critical differentiator is Scope (S).

Deployment pattern categories

Category 1: microservice/cloud-native (S:C) → 8.6 HIGH

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:H

Affected frameworks: Node.js, Go, Rust, Python async, all proxies/WAFs/API gateways

Why Scope = Changed:

  • Service mesh environments: Kong, Envoy, Caddy route traffic to downstream services
  • Kubernetes deployments: Service failure triggers cascading failures in dependent pods
  • API gateway patterns: Single point of failure for multiple backend services
  • Event-driven architectures: Failed message processors block entire pipelines

Real-world scenarios:

  • E-commerce checkout API fails → entire purchase flow down
  • Authentication service fails → all dependent microservices inaccessible
  • API gateway fails → entire backend infrastructure unreachable

Category 2: traditional/standalone (S:U) → 7.5 HIGH

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Affected frameworks: PHP/Laravel, Python Flask, Ruby/Rails, Elixir/Phoenix

Why Scope = Unchanged:

  • Monolithic deployments: Application failure affects only that application
  • Traditional hosting: Single server, single service pattern
  • Mixed deployment patterns: Some containerized, some bare-metal

Deployment contexts:

  • WordPress-style PHP applications on shared hosting
  • Ruby on Rails monoliths
  • Traditional Python web applications
  • Legacy enterprise deployments

Category 3: enterprise variable (6.5-8.6 MEDIUM to HIGH)

Affected frameworks: Java/Spring, .NET/ASP.NET Core

Scoring rationale:

  • Public APIs (PR:N): 8.6 HIGH, same vector as Category 1
  • Internal/authenticated APIs (PR:L): 6.5 MEDIUM, reduced attack surface
  • Enterprise security: Often deployed with additional layers (WAFs, network segmentation)

Why the range:

  • Java/Spring: Often in microservice architectures (8.6) but also monoliths (6.5)
  • .NET Core: Cloud-native (8.6) vs traditional Windows Server (6.5)

Category 4: infrastructure layer (S:C) → 8.6 HIGH

Affected components: nginx, Caddy, HAProxy, Kong, Envoy, ModSecurity, Coraza

Why Scope = Changed:

  • Reverse proxy failure: All backend services become unreachable
  • WAF failure: Entire protected infrastructure exposed
  • Load balancer failure: Multi-server deployments become single points of failure
  • API gateway failure: Microservice mesh coordination breaks down

Scoring decision tree

Slow JSON Stream vulnerability detected
│
├─ Framework resistant? → CVSS 0.0 (not vulnerable)
│
├─ Infrastructure component? → 8.6 HIGH (Changed Scope)
│   └─ (proxies, WAFs, API gateways)
│
├─ Microservice/async runtime? → 8.6 HIGH (Changed Scope)  
│   └─ (Node.js, Go, Rust, Python async)
│
├─ Enterprise framework?
│   ├─ Public API endpoint → 8.6 HIGH
│   └─ Authenticated endpoint → 6.5 MEDIUM
│   └─ (Java, .NET)
│
└─ Traditional framework → 7.5 HIGH (Unchanged Scope)
    └─ (PHP, Python Flask, Ruby, Elixir)

Temporal and environmental considerations

Temporal score adjustments:

  • Exploit Code Maturity: Functional (F): public PoC available in this repository
  • Remediation Level: Workaround (W): mitigations exist but no official patches
  • Report Confidence: Confirmed (C): verified across 41 targets

Environmental factors that may increase real-world impact:

  • High availability requirements: 24/7 services (finance, healthcare)
  • Peak traffic periods: Black Friday, breaking news events
  • Dependency chains: Critical services with many downstream consumers

Why this methodology is useful for security teams:

  1. Resource prioritization: Focus on 8.6 HIGH microservices first
  2. Risk assessment: Understanding which deployments have cascading failure
  3. Mitigation planning: Different approaches for different deployment patterns
  4. Compliance reporting: Framework-specific scores for vulnerability management systems

All 32 frameworks at a glance

Framework Ecosystem Tier Vulnerable? CVSS Score Evidence / Risk
php-laravel PHP/Apache 1 🔴 VULNERABLE 7.5 HIGH 258 MB/min RSS growth: RAM exhaustion in <2 min on a 512 MB container at C=64
dotnet-aspnet-mvc .NET/Kestrel 1 🔴 VULNERABLE 6.5-8.6 120 MB/min RSS growth
dotnet-aspnet .NET/Kestrel 1 🔴 VULNERABLE 6.5-8.6 66 MB/min RSS growth
java-quarkus Java/Vert.x 1 🔴 VULNERABLE 6.5-8.6 6.1 MB/min RSS growth
java-spring-webflux Java/Netty 1 🔴 VULNERABLE 6.5-8.6 3.5 MB/min RSS growth
java-spring Java/Tomcat 1 🔴 VULNERABLE 6.5-8.6 3.5 MB/min RSS growth
node-express-raw Node.js 1 🔴 VULNERABLE 8.6 HIGH 2.4 MB/min RSS growth
python-flask Python/gunicorn sync 3 🔴 VULNERABLE 7.5 HIGH 94% error rate, p99=4935 ms: service effectively down at C=64
ruby-rails Ruby/Puma 3 🔴 VULNERABLE 7.5 HIGH 100% error rate at C=64: service fully unavailable
elixir-phoenix Elixir 2 🔴 VULNERABLE 7.5 HIGH No body timeout: lightweight processes accumulate indefinitely
php-symfony PHP/fpm 2 🔴 VULNERABLE 7.5 HIGH No body timeout: process-per-request pool exhausted at C=~100
node-nestjs Node.js 2 🔴 VULNERABLE 8.6 HIGH No body timeout: event loop slots occupied indefinitely
node-express Node.js 2 🔴 VULNERABLE 8.6 HIGH No body timeout: event loop slots occupied indefinitely
node-koa Node.js 2 🔴 VULNERABLE 8.6 HIGH No body timeout: event loop slots occupied indefinitely
node-fastify Node.js 2 🔴 VULNERABLE 8.6 HIGH No body timeout: event loop slots occupied indefinitely
go-echo Go 2 🔴 VULNERABLE 8.6 HIGH No body timeout: goroutines accumulate indefinitely
go-gin Go 2 🔴 VULNERABLE 8.6 HIGH No body timeout: goroutines accumulate indefinitely
go-nethttp Go 2 🔴 VULNERABLE 8.6 HIGH No body timeout: goroutines accumulate indefinitely
go-nethttp-streaming Go 2 🔴 VULNERABLE 8.6 HIGH No body timeout: goroutines accumulate indefinitely
ruby-sinatra Ruby/Puma 2 🔴 VULNERABLE 7.5 HIGH No body timeout: thread pool exhausted at production concurrency
python-fastapi Python/uvicorn 2 🔴 VULNERABLE 8.6 HIGH No body timeout: async tasks accumulate indefinitely
python-fastapi-gunicorn Python/gunicorn 2 🔴 VULNERABLE 8.6 HIGH No body timeout: async workers accumulate indefinitely
python-flask-gevent Python 2 🔴 VULNERABLE 7.5 HIGH No body timeout: greenlets accumulate indefinitely
python-django Python/uvicorn 2 🔴 VULNERABLE 8.6 HIGH No body timeout: async tasks accumulate indefinitely
python-starlette Python/uvicorn 2 🔴 VULNERABLE 8.6 HIGH No body timeout: async tasks accumulate indefinitely
python-sanic Python 2 🔴 VULNERABLE 8.6 HIGH No body timeout: async tasks accumulate indefinitely
python-aiohttp Python 2 🔴 VULNERABLE 8.6 HIGH No body timeout: async tasks accumulate indefinitely
rust-axum Rust/tokio 2 🔴 VULNERABLE 8.6 HIGH No body timeout: async tasks accumulate indefinitely
rust-rocket Rust/tokio 2 🔴 VULNERABLE 8.6 HIGH No body timeout: async tasks accumulate indefinitely
rust-actix Rust/actix-rt 2 🔴 VULNERABLE 8.6 HIGH No body timeout: async tasks accumulate indefinitely
go-fiber Go/fasthttp 4 🟢 RESISTANT 0.0 RESISTANT: 4 KB read buffer limit, kills slow bodies in ~1 s
python-fastapi-streaming Python/ijson 4 🟢 RESISTANT 0.0 RESISTANT: streaming JSON parser with per-token dispatch, kills connections in ~1 s

Extended testbed: 9 additional targets (proxies, WAFs, API gateways)

Target Type Tier Vulnerable? CVSS Score Evidence / Risk
kong-ce API Gateway 1 🔴 VULNERABLE 8.6 HIGH 12.1 MB/min RSS growth: Tier 1 degradation at C=64
caddy-proxy Reverse Proxy 2 🔴 VULNERABLE 8.6 HIGH No body timeout: 1.0 MB/min RSS growth: exposed at production scale
nginx-proxy Reverse Proxy 2 🔴 VULNERABLE 8.6 HIGH No body timeout: 0.5 MB/min RSS growth: exposed at production scale
haproxy Load Balancer 2 🔴 VULNERABLE 8.6 HIGH No body timeout: 0.1 MB/min RSS growth: exposed at production scale
waf-modsecurity-nginx WAF 2 🔴 VULNERABLE 8.6 HIGH No body timeout: 0.6 MB/min RSS growth: exposed at production scale
waf-coraza WAF 2 🔴 VULNERABLE 8.6 HIGH No body timeout: minimal RSS growth: exposed at production scale
envoy-proxy Service Mesh 2 🔴 VULNERABLE 8.6 HIGH No body timeout: minimal RSS growth: exposed at production scale
traefik Reverse Proxy 4 🟢 RESISTANT 0.0 RESISTANT: timeout mechanism at ~60s, prevents holding connections indefinitely
waf-apache-modsec WAF 4 🟢 RESISTANT 0.0 RESISTANT: defensive timeout at 22s with early connection termination

Results by tier

RSS slope = resident memory growth during the attack (MB/min at C=64). p99 ratio = attack p99 latency ÷ baseline p99 (1.0 = no change; higher = worse). Evidence = what signals the experiment observed.

Tier 1: observed degradation at C=64 (RSS growth or latency spike)

These frameworks show measurable impact at just 64 connections, achievable from a single commodity host.

Framework Ecosystem RSS slope (MB/min) p99 ratio Evidence
php-laravel PHP/Apache mod_php 258 0.4× ¹ RSS growth (Rule 3) + 4% error rate
dotnet-aspnet-mvc .NET/Kestrel 120 1.1× RSS growth (Rule 3)
dotnet-aspnet .NET/Kestrel 66 1.4× RSS growth (Rule 3)
java-quarkus Java/Vert.x 6.1 1.5× RSS growth (Rule 3), borderline ³
java-spring Java/Tomcat 5.1 ³ — ² RSS growth (Rule 3) on payload O; other payloads below threshold

¹ php-laravel p99 drops during attack because workers are full and Apache rejects new probes faster than it serves them. The RSS growth is the real exposure signal. ² Java baseline p99 was elevated by JVM warmup (300 to 450 ms); attack p99 dropped to 14 to 17 ms after warmup. RSS growth is the reliable signal. ³ java-quarkus: RSS 4.5 to 7.0 MB/min across payloads, one above the 5 MB/min threshold. java-spring: only payload O reaches 5.1 MB/min; the remaining four payloads average 2.9 MB/min.

Tier 2: no body timeout (exploitable at production concurrency)

These 23 application frameworks (29 targets in total once the 6 Tier 2 infrastructure components are included) have no mechanism to close slow connections. Goroutines, event-loop slots, or thread-pool entries are occupied for the entire duration of the attack. At C=64 the concurrency ceiling is not reached and metrics appear stable. In production, where worker pools are typically exhausted at C=100 to 500, the same 64-connection attack saturates the pool and renders the service unresponsive.

Framework Ecosystem Worker model p99 ratio at C=64 Risk at C=256
elixir-phoenix Elixir lightweight processes 1.5× Medium
php-symfony PHP/fpm process-per-req 1.0× High
node-nestjs Node.js event loop 1.1× Medium
node-express Node.js event loop 1.6× Medium
node-koa Node.js event loop 1.5× Medium
node-fastify Node.js event loop 2.4× Medium
go-echo Go goroutine 1.4× Low–Medium
go-gin Go goroutine 1.4× Low–Medium
go-nethttp Go goroutine 1.1× Low–Medium
go-nethttp-streaming Go goroutine 1.5× Low–Medium
ruby-sinatra Ruby/Puma thread pool 1.4× Medium
python-fastapi Python/uvicorn async 1.2× Low–Medium
python-fastapi-gunicorn Python/gunicorn async workers 1.2× Low–Medium
python-flask-gevent Python greenlets 1.1× Low–Medium
python-django Python/uvicorn async 1.1× Low
python-starlette Python/uvicorn async 1.1× Low
python-sanic Python async 1.1× Low
python-aiohttp Python async 1.1× Low
rust-axum Rust/tokio async 2.5× Low
rust-rocket Rust/tokio async 1.6× Low
rust-actix Rust/actix-rt async 1.2× Low
java-spring-webflux Java/Netty goroutine-style 4.3× Medium
node-express-raw Node.js event loop 1.5× Medium

Tier 3: hard failure / service down (worst outcome)

These frameworks do not degrade gracefully; they crash. The service is 100% unavailable to legitimate traffic. This is worse than Tier 1 degradation: there is no partial availability, no slow responses, the service is simply down.

Framework Ecosystem Behaviour
python-flask Python/gunicorn sync Worker processes block, then queue overflows: 94% error rate at C=64, p99 = 4935 ms. Service effectively unavailable.
ruby-rails Ruby/Puma Thread pool exhaustion causes 100% error rate at C=64. No requests served.

Tier 4: effective protection

Only 2 of the 32 application frameworks resist the attack by default (4 of 41 targets once the resistant infrastructure components traefik and waf-apache-modsec are counted). Both application frameworks use architectural mechanisms that make slow-body holding structurally impossible.

Framework Ecosystem Why it resists
go-fiber Go/fasthttp 4 KB read buffer limit, kills any slow body in ~1 s. Note: probe requests also fail (100% error rate) because fasthttp's timeout applies to all connections; the server is not overwhelmed, it is enforcing a strict limit.
python-fastapi-streaming Python/ijson Streaming JSON parser: handler is invoked per token and can return early. Attacker connections killed in ~1 s.

Results by ecosystem

Ecosystem Frameworks Tier 1 (degradation) Tier 2 (no timeout) Tier 3 (crash) Tier 4 (resists)
PHP 2 1 (Laravel) 1 (Symfony)
.NET 2 2
Java 3 3
Node.js 5 1 (express-raw) 4
Go 5 4 1 (fiber)
Elixir 1 1
Ruby 2 2
Rust 3 3
Python 9 6 1 (flask) 2 (streaming, aiohttp*)

Results by ecosystem (updated with extended targets)

Ecosystem Total Tier 1 Tier 2 Tier 3 Tier 4 Vulnerable? Overall CVSS
Node.js 5 1 4 0 0 🔴 VULNERABLE (5/5) 8.6 HIGH
Go 5 0 4 0 1 🟡 MOSTLY VULN (4/5) 6.9 HIGH¹
Java 3 3 0 0 0 🔴 VULNERABLE (3/3) 6.5-8.6
.NET 2 2 0 0 0 🔴 VULNERABLE (2/2) 6.5-8.6
PHP 2 1 1 0 0 🔴 VULNERABLE (2/2) 7.5 HIGH
Ruby 2 0 0 2 0 🔴 VULNERABLE (2/2) 7.5 HIGH
Rust 3 0 3 0 0 🔴 VULNERABLE (3/3) 8.6 HIGH
Python 9 0 6 1 2 🟡 MOSTLY VULN (7/9) 7.8 HIGH²
Proxies/WAFs/Gateways 9 1 6 0 2 🟡 MOSTLY VULN (7/9) 7.4 HIGH³

Footnotes: ¹ Go: 4 frameworks × 8.6 + 1 × 0.0 = weighted avg 6.9 ² Python: 6 async × 8.6 + 1 traditional × 7.5 + 2 resistant × 0.0 = weighted avg 7.8
³ Proxies: 7 vulnerable × 8.6 + 2 resistant × 0.0 = weighted avg 7.4


What protects you, and what doesn't

Defense Stops the attack? Notes
client_header_timeout (nginx) 🟢 RESISTANT Only covers the header phase
client_body_timeout 60s (nginx default) 🟢 RESISTANT Resets on each chunk, so the attacker drips 1 B every 59 s
client_body_timeout 5s + limit_req 🔴 VULNERABLE Effective; add minimum-rate clause for completeness
Apache RequestReadTimeout body=10,MinRate=100 🔴 VULNERABLE MinRate=100 B/s kills a 1 B/s drip immediately
ASP.NET Core MinRequestBodyDataRate (default: null) 🔴 VULNERABLE Disabled by default, must be set explicitly
Go http.Server{ReadTimeout: 10s} 🔴 VULNERABLE Covers full request including body
Node.js server.requestTimeout (≥ Node 18) 🔴 VULNERABLE Absolute deadline per request
Spring Boot server.tomcat.connection-timeout 🔴 VULNERABLE Set to 10 000 ms
Content-Length enforcement 🟢 RESISTANT Chunked encoding has no declared length
client_max_body_size / WAF size limits 🟢 RESISTANT Enforced only after full body arrives
IP-based rate limiting ⚠️ Partial Limits scale; doesn't stop a single slow connection
Streaming JSON parser (ijson, json.Decoder.Token) 🔴 VULNERABLE Handler invoked per token; can return early

Key findings

1. PHP/Laravel exhausts RAM in under 2 minutes at 64 connections. With only 64 connections sending at <1 kbps, php-laravel accumulates 258 MB/min of RSS at C=64. A standard 512 MB container is exhausted in less than 2 minutes. This is the most severe single-framework result: a commodity host is sufficient to exhaust an unhardened worker pool, with no tool beyond slowjson.

2. 37 of 41 targets (90%) are exposed by default. The 32 application frameworks showed 94% exposure (30/32); two resist: go-fiber (fasthttp buffer ceiling) and python-fastapi-streaming (streaming parser). Of the 9 infrastructure components, 7 are exposed; Traefik and Apache ModSecurity resist by enforcing body-read timeouts (60 s and 22 s respectively). Even infrastructure designed to protect applications can be bypassed. The 29 Tier 2 targets are not "safe at C=64". They have no body timeout whatsoever, so connections accumulate indefinitely: goroutines, event-loop slots, and thread-pool entries are held open until the worker pool collapses. Production deployments typically exhaust their pools at C=100 to 500, well within the reach of a single commodity host.

3. Tier 3 (Flask, Rails) is the worst outcome. A 94% error rate on Flask and 100% error rate on Rails at C=64 means the service is fully down, not merely slow. This is a harder impact than Tier 1 degradation.

4. Async runtimes (Tier 2) are not immune, just cheaper to attack. A blocked coroutine costs ~2 KB vs ~1 MB for a thread. At C=64 the impact is near zero; at C=10 000 (reachable from a single server) the impact is equivalent. The exposure is real; the required investment is higher.

5. The two natural defenses are streaming dispatch and hard read-buffer limits. go-fiber kills connections in ~1 s via fasthttp's buffer ceiling. python-fastapi-streaming kills them via per-token handler dispatch. Every other framework has no such mechanism by default.

6. Payload shape is irrelevant. All four payload shapes (array of objects, nested, flat, large string) produce identical tier classification in 30 of 32 frameworks. The attack does not depend on a specific JSON structure.


Mitigations (quick reference)

nginx

client_body_timeout 10s;
# combine with limit_req to cap concurrent slow senders

Apache httpd

RequestReadTimeout body=10,MinRate=100

ASP.NET Core / Kestrel ← most impacted ecosystem

options.Limits.MinRequestBodyDataRate = new MinDataRate(
    bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));

Go net/http

srv := &http.Server{ReadTimeout: 10 * time.Second}

Node.js ≥ 18

server.requestTimeout = 10_000;

Spring Boot

server.tomcat.connection-timeout: 10000

Application level Use a streaming JSON parser: Python → ijson, Node.js → stream-json, Go → json.Decoder.Token().


Running the testbed

Read ETHICS.md first. Only test servers you own.

git clone https://github.com/cr0hn/slowjson
cd slowjson
pip install -e cli/
make up        # start all 32 PoC servers
make matrix    # run full experiment (~12 h)
less results/report.md

Single target:

slowjson attack http://localhost:8001/ingest \
    --payload A --bytes-per-tick 1 --tick-interval-ms 1000 \
    --connections 64 --duration 90 \
    --probe-url http://localhost:8001/healthz \
    --docker-container slowjson-python-fastapi \
    --output results/test.json \
    --i-own-this-server

Severity: CVSS 6.5-8.6 HIGH

For a typical microservice deployment (Kubernetes, service mesh, API gateway): CVSS 8.6 HIGH.
For a standalone deployment: CVSS 7.5 HIGH.

Most production APIs run behind a service mesh or API gateway. Taking down a single service cascades to all callers, raising the scope from Unchanged to Changed:

Microservice / cloud deployment (most common):
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:H  →  8.6 HIGH

Standalone deployment (conservative baseline):
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H  →  7.5 HIGH
Metric Value Reason
Attack Vector Network (N) JSON API endpoint reachable over the network
Attack Complexity Low (L) Standard HTTP chunked transfer; no authentication, no special conditions, no prior knowledge required
Privileges Required None (N) REST JSON APIs typically accept unauthenticated POST requests
User Interaction None (N) Fully automated, no victim action needed
Scope Changed (C) for microservices Downstream services fail when an upstream is exhausted
Confidentiality None (N) No data exfiltrated
Integrity None (N) No data modified
Availability High (H) Service fully unavailable to legitimate users

Score by scenario:

Scenario Vector Score
Microservice / k8s / API gateway (typical) S:U → S:C 8.6 HIGH
Standalone, unauthenticated endpoint baseline 7.5 HIGH
Endpoint requires authentication PR:N → PR:L 6.5 MEDIUM
Tier 4 framework (resistant by default) 0.0

Temporal score (PoC tool publicly available, workaround mitigations exist): ~7.7 HIGH

CVE assignment is recommended for all Tier 1 and Tier 3 frameworks. For Tier 2, CVE relevance depends on the deployer's concurrency ceiling, but the exposure is real in all cases.


The Reddit hate

I posted this project on r/netsec. Nobody gets a free pass there, no matter how solid the research is. If you're reading this with your own criticism in mind, you wouldn't be the first — here's what the post got, and how I responded.

"This is still a classic slowloris." — u/TheG0AT0fAllTime

Slowloris never completes the HTTP request; it stalls on headers. This attack sends complete headers with a valid Content-Length, then drips the body. The pressure lands on the application framework's JSON parser, not the HTTP server. 23 of 32 frameworks tested have no body timeout configured by default. Where it's slow changes the attack surface, the affected layer, and what mitigates it.

"LLM convincing an unqualified operator. nginx doesn't pass to PHP until the request is complete." — u/notR1CH

The PHP-FPM claim is correct but beside the point: the testbed covers FastAPI, Express, Spring Boot, Rails, Go, and .NET, each with its own HTTP server, not just PHP behind nginx. nginx itself is in the extended testbed (port 8033) — measured behavior is that it buffers the body before forwarding, so nginx keeps the slow connections open itself. Each 1-byte tick resets client_body_timeout. The protection nginx gives PHP becomes nginx's own exposure. W3Techs (June 2025) puts nginx at ~34% of sites with a known server; API backends on Lambda, Cloud Run, App Service, Heroku, Railway, and Render typically have no nginx in the path by default. 34% is a ceiling, not a floor. "LLM convincing an unqualified operator" is a social argument, not a technical one — the setup is reproducible: 32 frameworks, 4 payloads, 3 repeats, 4 evidence-based verdict rules, and a 9-target proxy/WAF suite. The same "just an AI-assisted amateur" argument was made against IDEs, compilers, and static analyzers. It hasn't aged well any of those times.

"You can do this with most streaming platforms. Finite resource consumption due to buffer exploits. Nothing new here." — anonymous comment

There is no buffer overflow. The attack sends a valid JSON body at 1 byte/second over a legitimate connection. 64 connections are enough to degrade or kill most tested frameworks. The distinction from RUDY (generic slow POST bodies) is the specific target: JSON API frameworks that parse incrementally and have no body timeout by default. If there's a public reproduction on a streaming platform, happy to see it.


Disclosure

This research follows a full-disclosure posture. The findings, the complete attack CLI, and a reproducible Docker testbed for all 41 targets are published openly. The exposure comes from default configuration rather than a hidden bug, and every mitigation in this README is available to operators today.

Anyone can reproduce the results with make up and make matrix. If you maintain one of the tested frameworks or run an affected deployment, the Mitigations section lists the one-line config change that closes the gap.


License

Component License
Code (cli/, servers/, orchestrator/) Apache-2.0
Paper and figures (paper/) CC-BY-4.0