Skip to content

Sync high-value upstream fixes: response queue poisoning (GHSA-35p6-xmwp-9g52), multi-byte setEncoding, null queue guard - #5

Merged
ronag merged 4 commits into
masterfrom
fix/upstream-response-queue-poisoning
Jul 1, 2026
Merged

Sync high-value upstream fixes: response queue poisoning (GHSA-35p6-xmwp-9g52), multi-byte setEncoding, null queue guard#5
ronag merged 4 commits into
masterfrom
fix/upstream-response-queue-poisoning

Conversation

@ronag

@ronag ronag commented Jul 1, 2026

Copy link
Copy Markdown
Member

Cherry-picks three upstream fixes that landed after our last sync and that apply to this fork's scope (h1-only core client; no h2/interceptors/web). Each is ported and adapted to our diverged code, with regression tests.

1. 🔴 Security — response queue poisoning (GHSA-35p6-xmwp-9g52)

Upstream 6ea54ef8, 89323ff9, c9fbe9d2, ac5394b8.

Our client-h1.js had no idle-socket validation, so a misbehaving/malicious peer could write an unsolicited response onto an idle keep-alive socket and poison the next request's response.

  • Proactively read + open a one-tick validation window (native, unref'd timer) before reusing a previously-used idle socket; busy() holds off dispatch while validation is pending.
  • New kRunning === 0 guards in onMessageBegin/onHeadersComplete tear the socket down ('bad response') if bytes arrive while nothing is inflight.
  • Validation state reset in writeH1 and cleared on socket close.
  • Regression test: test/response-queue-poisoning.js (from upstream).

2. 🟡 Correctness — multi-byte UTF-8 in setEncoding()

Upstream 74a2299e (nodejs#5003).

setEncoding() set _readableState.encoding directly, so no StringDecoder was installed and a multi-byte char split across a chunk boundary decoded to U+FFFD. Now delegates to super.setEncoding(), preserving raw Buffers under kPreservedBuffer for byte-accurate .text()/.json().

  • Regression test: test/readable-setencoding-multibyte.js.

3. 🟡 Robustness — null queue entries in errorRequest loops

Upstream 6df53c50 (nodejs#5410).

Skip already-nulled queue slots before util.errorRequest in kDestroy/onError — relevant now that our pipelined-error handling splices/nulls queue entries.

Out of scope (not ported)

h2, fetch, websocket, cookies, cache-web, proxy/socks, dns/retry/mock interceptors — none exist in this fork. e71624eb (nodejs#5356) is already covered differently by our 86330728.

Testing

eslint clean; 190 tests pass, 0 fail in node:25 Docker across the new tests plus the full readable / pipelining / keep-alive / request / client / dispatcher / pool / socket blast radius.

🤖 Generated with Claude Code

ronag and others added 3 commits July 1, 2026 13:13
…-xmwp-9g52)

Port the response-queue-poisoning defense from upstream (6ea54ef, 89323ff,
c9fbe9d, ac5394b), adapted to this fork's h1-only client and older parser
handler interface.

A misbehaving or malicious peer can write an unsolicited/early response onto an
idle keep-alive socket. Without validation the next request dispatched on that
socket reads the stale bytes as its response, poisoning the response queue.

Before binding a pending request to a previously-used idle socket, resumeH1 now
proactively reads from it once and opens a one event-loop-tick validation window
(native timer, unref'd). busy() reports the socket as busy while validation is
pending so no request is dispatched meanwhile. Any bytes that arrive while
nothing is inflight are turned into a 'bad response' socket teardown by the new
kRunning === 0 guards in onMessageBegin/onHeadersComplete, so the socket is
discarded and a fresh one is used. Validation state is reset in writeH1 (request
bound) and cleared on socket close.

Adds test/response-queue-poisoning.js from upstream as a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ding()

Port upstream 74a2299 (nodejs#5003). setEncoding() previously assigned
_readableState.encoding directly, so no StringDecoder was installed and each
chunk was decoded independently — a multi-byte UTF-8 sequence split across a
chunk boundary produced U+FFFD replacement characters on the for-await /
on('data') path.

Delegate to super.setEncoding() so Node installs a StringDecoder that stitches
split sequences correctly. Because that also rewrites already-buffered chunks
into decoded strings (which would break Buffer.concat on the consume path),
preserve the raw Buffers first under kPreservedBuffer and prefer them in
consumeStart() for byte-accurate .text()/.json().

Adds test/readable-setencoding-multibyte.js as a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port upstream 6df53c5 (nodejs#5410). When failing the queue in kDestroy and onError,
completed slots may already be nulled out (and this fork's own pipelined-error
handling splices/nulls queue entries too). Skip null entries before calling
util.errorRequest so a torn-down slot can't throw while erroring the queue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR syncs three upstream fixes into this fork: a security hardening for HTTP/1.1 keep-alive socket reuse (response queue poisoning / GHSA-35p6-xmwp-9g52), correctness for multi-byte UTF-8 handling in BodyReadable#setEncoding(), and robustness improvements for null queue entries during error teardown.

Changes:

  • Add idle-socket validation + “bad response while idle” teardown to prevent response queue poisoning on keep-alive reuse.
  • Fix BodyReadable#setEncoding() to delegate to Readable#setEncoding() (StringDecoder) while preserving raw bytes for .text() / .json() consume paths.
  • Guard util.errorRequest() loops against already-nulled queue entries; add regression tests for both security and encoding behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
lib/dispatcher/client-h1.js Adds idle-socket validation state machine and inflight guards to prevent response queue poisoning on socket reuse.
lib/api/readable.js Adjusts setEncoding() to use Node’s decoder and preserves raw buffers for byte-accurate consumption APIs.
lib/dispatcher/client.js Skips null/undefined queue slots when erroring requests during destroy/error flows.
test/response-queue-poisoning.js Regression test ensuring unsolicited bytes on an idle keep-alive socket don’t poison the next request’s response.
test/readable-setencoding-multibyte.js Regression test ensuring UTF-8 multibyte sequences split across chunks decode correctly after setEncoding('utf8').

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/dispatcher/client-h1.js
Comment thread lib/api/readable.js
Two issues raised on the idle-socket-validation / setEncoding changes (both
also present upstream):

- client-h1.js: kIdleSocketValidation could stay stuck at 1/2 if the pending
  request that triggered validation was aborted before being written, causing
  the socket to skip the poisoning check on its next reuse. Reset validation
  state in resumeH1 when the socket goes fully idle (kSize === 0) so it is
  revalidated on reuse. (GHSA-35p6-xmwp-9g52)

- readable.js: consumeStart() is a plain function, so `this` is undefined in
  strict mode; the state.endEmitted fast-path dereferenced this[kConsume] /
  this._readableState and would throw if reached. Use the consume argument and
  captured state instead. (Currently guarded by the isUnusable check in
  consume(), but corrected defensively.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread test/readable-setencoding-multibyte.js
@ronag
ronag merged commit b5d7cad into master Jul 1, 2026
1 check passed
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.

2 participants