Summary
An unauthenticated POST to any OpenAPI-validated endpoint, including the anonymous POST /edge/validate and POST /edge/issue-token, crashes the entire Unleash server with one request body of deeply-nested JSON.
When request-body validation fails, Unleash builds the error message by calling JSON.stringify on the raw offending value taken from the request body. A value nested a few thousand levels deep makes JSON.stringify recurse past the V8 call-stack limit and throw RangeError: Maximum call stack size exceeded.
The throw is synchronous inside an Express error-handling middleware that has no try/catch, and the process registers no uncaughtException handler (only unhandledRejection). Node terminates the process with exit(1).
The body parser sets no JSON nesting-depth limit, the payload (about 10 KB at depth 5000) stays far under the 100 KB body-size limit, and the same error formatter serves every API route, so the trigger is route-independent.
Result: a remote attacker with no account, token, or cookie takes the whole server offline with a single 10 KB request, and keeps it offline by replaying it.
Affected
Unleash OSS server, confirmed live-exploitable on v8.0.0 (unleashorg/unleash-server:8.0.0).
Vulnerable code is the shared request-validation error path (src/lib/error/bad-data-error.ts genericErrorMessage), present on current main.
Reachable on a stock install: no feature flag, no setting, no authentication, no CSRF token, and no cookie required.
The 100 KB request-body size limit does not mitigate it; the crashing payload is about 10 KB.
Root cause
On validation failure the error formatter serializes the raw offending request value with an unguarded const youSent = JSON.stringify(propertyValue) (src/lib/error/bad-data-error.ts:75), where propertyValue is read straight from the request body via lodash.get (bad-data-error.ts:123). A deeply-nested array or object makes JSON.stringify recurse once per level and throw RangeError: Maximum call stack size exceeded. fromOpenApiValidationErrors (bad-data-error.ts:158) runs from the Express error middleware openAPIValidationMiddleware (src/lib/routes/controller.ts:68), which calls it with no try/catch (controller.ts:70), while the controller's own try/catch (controller.ts:103) wraps only the route handler. The process registers only process.on('unhandledRejection') (src/lib/server-impl.ts:274) and no uncaughtException handler, so the synchronous throw terminates Node with exit(1). The same unguarded pattern exists at fromJoiError (bad-data-error.ts:175) and at handleErrors (src/lib/routes/util.ts:42), whose own comment records that JSON.stringify(finalError.details) "also hangs" (util.ts:63).
Reproduction
unleashorg/unleash-server:8.0.0 started from the repository docker-compose.yml, default config, no credentials, server at http://localhost:4242.
- A shallow body returns a normal 400 and the server stays up.
curl -s -X POST http://localhost:4242/edge/validate \
-H 'Content-Type: application/json' -d '{"tokens":[[]]}'
# HTTP 400 BadDataError: "The `/body/tokens/0` property must be string. You sent []."
- A single anonymous body nested 5000 levels deep (10013 bytes) crashes the process.
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:4242/edge/validate \
-H 'Content-Type: application/json' \
--data-binary "$(python3 -c 'd=5000;print("{\"tokens\":["+"["*d+"]"*d+"]}")')"
# 000 (connection dropped mid-response; Node exited)
- The server is gone and does not recover on its own.
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:4242/health
# 000
docker inspect repo-web-1 --format '{{.State.Status}} restarts={{.RestartCount}}'
# exited restarts=0
Live-verified: one anonymous 10013-byte request to /edge/validate drove /health from 200 to 000; the container logs showed the RangeError stack through bad-data-error and openAPIValidationMiddleware, and the process did not restart. The crash threshold is about 4000 to 4500 nesting levels; depth 5000 crashes reliably. The same crash reproduces on POST /edge/issue-token with a nested object, confirming route and shape independence.
Impact
- Single 10 KB request with no account, token, or cookie takes the entire server offline.
- Replaying the request (kilobits per restart interval) sustains a near-total outage; with no restart policy the first request is a permanent kill.
- Feature-flag evaluation sits on the request path of dependent applications, so the outage propagates to them.
- Every OpenAPI-validated endpoint is a trigger, including anonymous ones.
Credit
Jan Kahmen, turingpoint (jan@turingpoint.de)
Summary
An unauthenticated
POSTto any OpenAPI-validated endpoint, including the anonymousPOST /edge/validateandPOST /edge/issue-token, crashes the entire Unleash server with one request body of deeply-nested JSON.When request-body validation fails, Unleash builds the error message by calling
JSON.stringifyon the raw offending value taken from the request body. A value nested a few thousand levels deep makesJSON.stringifyrecurse past the V8 call-stack limit and throwRangeError: Maximum call stack size exceeded.The throw is synchronous inside an Express error-handling middleware that has no try/catch, and the process registers no
uncaughtExceptionhandler (onlyunhandledRejection). Node terminates the process withexit(1).The body parser sets no JSON nesting-depth limit, the payload (about 10 KB at depth 5000) stays far under the 100 KB body-size limit, and the same error formatter serves every API route, so the trigger is route-independent.
Result: a remote attacker with no account, token, or cookie takes the whole server offline with a single 10 KB request, and keeps it offline by replaying it.
Affected
Unleash OSS server, confirmed live-exploitable on v8.0.0 (
unleashorg/unleash-server:8.0.0).Vulnerable code is the shared request-validation error path (
src/lib/error/bad-data-error.tsgenericErrorMessage), present on currentmain.Reachable on a stock install: no feature flag, no setting, no authentication, no CSRF token, and no cookie required.
The 100 KB request-body size limit does not mitigate it; the crashing payload is about 10 KB.
Root cause
On validation failure the error formatter serializes the raw offending request value with an unguarded
const youSent = JSON.stringify(propertyValue)(src/lib/error/bad-data-error.ts:75), wherepropertyValueis read straight from the request body vialodash.get(bad-data-error.ts:123). A deeply-nested array or object makesJSON.stringifyrecurse once per level and throwRangeError: Maximum call stack size exceeded.fromOpenApiValidationErrors(bad-data-error.ts:158) runs from the Express error middlewareopenAPIValidationMiddleware(src/lib/routes/controller.ts:68), which calls it with no try/catch (controller.ts:70), while the controller's own try/catch (controller.ts:103) wraps only the route handler. The process registers onlyprocess.on('unhandledRejection')(src/lib/server-impl.ts:274) and nouncaughtExceptionhandler, so the synchronous throw terminates Node withexit(1). The same unguarded pattern exists atfromJoiError(bad-data-error.ts:175) and athandleErrors(src/lib/routes/util.ts:42), whose own comment records thatJSON.stringify(finalError.details)"also hangs" (util.ts:63).Reproduction
unleashorg/unleash-server:8.0.0started from the repositorydocker-compose.yml, default config, no credentials, server athttp://localhost:4242.Live-verified: one anonymous 10013-byte request to
/edge/validatedrove/healthfrom 200 to 000; the container logs showed theRangeErrorstack throughbad-data-errorandopenAPIValidationMiddleware, and the process did not restart. The crash threshold is about 4000 to 4500 nesting levels; depth 5000 crashes reliably. The same crash reproduces onPOST /edge/issue-tokenwith a nested object, confirming route and shape independence.Impact
Credit
Jan Kahmen, turingpoint (jan@turingpoint.de)