fix(lsp): add per-message read timeout to prevent DoS in _read_message() #10650

Merged
HAL9000 merged 5 commits from bugfix/m3.6.0-lsp-server-dos-message-read-timeout into master 2026-06-05 22:08:32 +00:00

5 Commits

Author SHA1 Message Date
HAL9000 22c3cddf08 test(lsp): cover the select-based read-body timeout branch via os.pipe()
CI / lint (pull_request) Successful in 39s
CI / quality (pull_request) Successful in 51s
CI / typecheck (pull_request) Successful in 1m9s
CI / build (pull_request) Successful in 32s
CI / helm (pull_request) Successful in 37s
CI / security (pull_request) Successful in 1m34s
CI / push-validation (pull_request) Successful in 31s
CI / unit_tests (pull_request) Successful in 4m38s
CI / docker (pull_request) Successful in 1m54s
CI / integration_tests (pull_request) Successful in 8m35s
CI / coverage (pull_request) Successful in 9m1s
CI / status-check (pull_request) Successful in 3s
The DoS mitigation added in db389a730 wraps each message-body read in
``select()`` so a stalled client cannot pin the server forever. The
existing transport tests route through MockLspTransport, whose
``BytesIO`` raises ``UnsupportedOperation`` on ``fileno()`` -- so
``_read_body_with_timeout`` always falls through the BytesIO fast
path and the actual ``select()``-based mitigation code (the part
that runs in production) is never executed. That left ~19 new lines
uncovered, dragging total coverage below the 96.5% floor and
failing the coverage gate.

Two new scenarios drive the helper through an ``os.pipe()`` whose
read fd satisfies ``fileno()``, so ``use_select`` is True and the
real DoS-protection path runs:

* ``timeout=0.0`` makes the deadline already past on the first
  iteration, exercising the ``if timeout <= 0:`` early-exit warning.
* ``timeout=0.05`` lets ``select.select()`` run and time out with no
  ready descriptors, exercising the ``if not ready:`` warning.

Both paths log ``lsp.transport.read_timeout`` and return ``None``,
matching the production behaviour the helper was added to provide.

ISSUES CLOSED: #5566
2026-06-05 17:25:35 -04:00
controller-ci-rerun c3de4921bb chore: re-trigger CI [controller]
CI / lint (pull_request) Successful in 42s
CI / helm (pull_request) Successful in 37s
CI / build (pull_request) Successful in 39s
CI / quality (pull_request) Successful in 54s
CI / push-validation (pull_request) Successful in 52s
CI / typecheck (pull_request) Successful in 1m13s
CI / security (pull_request) Successful in 1m23s
CI / unit_tests (pull_request) Successful in 5m53s
CI / docker (pull_request) Successful in 1m40s
CI / integration_tests (pull_request) Successful in 12m18s
CI / coverage (pull_request) Failing after 12m9s
CI / status-check (pull_request) Failing after 4s
2026-06-04 21:00:09 -04:00
controller-ci-rerun 1586901fc9 chore: re-trigger CI [controller] 2026-06-04 21:00:09 -04:00
HAL9000 22de62b930 style(lsp): fix ruff formatting in lsp_server_stub_steps.py
Remove extra blank lines and apply ruff auto-format to features/steps/lsp_server_stub_steps.py to fix the CI lint failure.
2026-06-04 21:00:09 -04:00
HAL9000 db389a7304 fix(lsp): add per-message read timeout to prevent DoS in _read_message()
Resolves the DoS vulnerability in LspServer._read_message() where a
malicious client could send a valid Content-Length header but stall
without delivering the body, blocking the server indefinitely.

Changes:
- Add MESSAGE_READ_TIMEOUT constant (default: 30 seconds)
- Add _read_body_with_timeout() method using select() for timeout enforcement
- Add read_timeout constructor parameter for configurability
- Replace blocking self._input.read(content_length) with timeout-based read
- Remove the SEC: comment that documented the vulnerability
- Add BDD tests covering timeout behavior and constant export

The fix uses select() for streams with a real file descriptor (e.g.
sys.stdin) and falls back to direct blocking read for in-memory streams
(e.g. io.BytesIO used in tests), preserving full test compatibility.

Closes #7083
2026-06-04 21:00:09 -04:00