Skip to content

fix(security): contain worker death on control-byte headers (closes #577) - #598

Merged
s2x merged 5 commits into
masterfrom
fix/issue-577-control-byte-worker-dos
Jul 27, 2026
Merged

fix(security): contain worker death on control-byte headers (closes #577)#598
s2x merged 5 commits into
masterfrom
fix/issue-577-control-byte-worker-dos

Conversation

@s2x

@s2x s2x commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Description

Closes #577

A single control byte in any request header value killed the Workerman worker process (remote unauthenticated DoS). The request lifecycle in HttpRequestHandler::__invoke() had no try/catch, and ServerWorker never assigned $connection->errorHandler, so Workerman called Worker::stopAll(250).

Changes

  • Wrap the full request lifecycle in HttpRequestHandler::__invoke() with try/catch; client input errors → 400, server faults → 500
  • Nested try/catch around error-response sendResponse() so send failure cannot escape (doTerminate + reboot still run)
  • Introduce ClientInputExceptionInterface marker; MalformedRequestException (new) and FileUploadValidationException implement it
  • RequestConverter throws MalformedRequestException instead of bare \InvalidArgumentException
  • ServerWorker::onConnect installs $connection->errorHandler backstop (log + timer cleanup + close)
  • Client errors logged at debug (no flood); server faults at error + error_log fallback
  • Unit tests for control-byte path, middleware throws, classification, doTerminate/reboot on failure, soak 10k, backstop behavior

Changelog

Code Review

  • Passed subagent code review (round 1: 16 findings fixed; round 2: clean)
  • All review comments addressed

Piotr Hałas added 3 commits July 27, 2026 19:08
…577)

Wrap the entire request lifecycle in HttpRequestHandler::__invoke()
in a try/catch so that any throwable — from request conversion
(control byte in a header value), a middleware, response conversion,
or response preparation — is turned into a 400 (client error) or
500 (server fault) response instead of escaping into Workerman's
TcpConnection error handler, which terminates the worker process.

Add a defence-in-depth backstop: ServerWorker::onConnect() now
installs $connection->errorHandler so that any throwable the handler
misses closes the connection cleanly instead of calling
Worker::stopAll(250).

doTerminate() and the reboot check still run on the failure path
(see #572). Client errors (\InvalidArgumentException,
FileUploadValidationException) are logged at debug level to prevent
log flooding by an unauthenticated attacker; server faults are
logged at error level with the full exception.
Blocker: wrap error-response send in nested try/catch so a throw
from sendResponse() during error handling does not escape __invoke()
(skipping doTerminate/reboot and reaching Workerman's error handler).

Major: tighten isClientError classification — introduce
ClientInputExceptionInterface marker implemented by
MalformedRequestException (new) and FileUploadValidationException.
RequestConverter now throws MalformedRequestException instead of
bare \InvalidArgumentException, so a middleware throwing
\InvalidArgumentException is correctly classified as a 500 server
fault, not a 400 client error.

Major: backstop errorHandler now logs to error_log and cleans up
per-connection timers (keepaliveTimerId, connectionTimerId) before
closing, instead of silently swallowing the throwable.

Major: replace tautology in soak test with real count + per-response
400 assertion.

Minor: strengthen doTerminate/reboot tests using spy reboot strategy
mock with expects(once()). Rename test methods dropping misleading
'KeepsWorkerAlive' suffix (unit tests cannot verify liveness).
@s2x s2x self-assigned this Jul 27, 2026
Piotr Hałas added 2 commits July 27, 2026 20:12
CI rector (PHP 8.2) flags RemoveDeadInstanceOfAssertRector on the
DateInterval assert after createFromDateString('+1 hour').
- Assert FileUploadValidationException/MalformedRequestException from
  RequestConverter without middleware injection
- Route incomplete multipart files through HttpRequestHandler to 400
- Spawn a real single-worker process and prove PID stability under
  control-byte requests, 10k soak, and errorHandler-only backstop
@s2x
s2x merged commit 0cef5f2 into master Jul 27, 2026
12 checks passed
@s2x
s2x deleted the fix/issue-577-control-byte-worker-dos branch July 27, 2026 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] A single control byte in any request header kills the worker process (remote unauthenticated DoS)

1 participant