Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/coverage-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ permissions:
jobs:
coverage-windows:
if: github.event.pull_request.draft == false
runs-on: windows-2025
runs-on: windows-2022
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ release.
</tr>
<tr>
<td valign="top">
<b><a href="doc/changelogs/CHANGELOG_V22.md#22.23.0">22.23.0</a></b><br/>
<b><a href="doc/changelogs/CHANGELOG_V22.md#22.23.1">22.23.1</a></b><br/>
<a href="doc/changelogs/CHANGELOG_V22.md#22.23.0">22.23.0</a><br/>
<a href="doc/changelogs/CHANGELOG_V22.md#22.22.3">22.22.3</a><br/>
<a href="doc/changelogs/CHANGELOG_V22.md#22.22.2">22.22.2</a><br/>
<a href="doc/changelogs/CHANGELOG_V22.md#22.22.1">22.22.1</a><br/>
Expand Down
13 changes: 13 additions & 0 deletions doc/changelogs/CHANGELOG_V22.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</tr>
<tr>
<td>
<a href="#22.23.1">22.23.1</a><br/>
<a href="#22.23.0">22.23.0</a><br/>
<a href="#22.22.3">22.22.3</a><br/>
<a href="#22.22.2">22.22.2</a><br/>
Expand Down Expand Up @@ -72,6 +73,18 @@
* [io.js](CHANGELOG_IOJS.md)
* [Archive](CHANGELOG_ARCHIVE.md)

<a id="22.23.1"></a>

## 2026-06-23, Version 22.23.1 'Jod' (LTS), @RafaelGSS

This release includes a fix for an unexpected behavior introduced
by the recent security release (22.23.0).

### Commits

* \[[`41d2ee13be`](https://github.com/nodejs/node/commit/41d2ee13be)] - **build**: switch coverage-windows to `windows-2022` (Richard Lau) [#63940](https://github.com/nodejs/node/pull/63940)
* \[[`eaa292549e`](https://github.com/nodejs/node/commit/eaa292549e)] - **http**: avoid stream listeners on idle agent sockets (Matteo Collina) [#64004](https://github.com/nodejs/node/pull/64004)

<a id="22.23.0"></a>

## 2026-06-18, Version 22.23.0 'Jod' (LTS), @aduh95
Expand Down
66 changes: 59 additions & 7 deletions lib/_http_agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,22 @@ const {
filterEnvForProxies,
} = require('internal/http');
const { AsyncResource } = require('async_hooks');
const { async_id_symbol } = require('internal/async_hooks').symbols;
const {
async_id_symbol,
owner_symbol,
} = require('internal/async_hooks').symbols;
const {
getLazy,
kEmptyObject,
once,
} = require('internal/util');
const {
onStreamRead,
} = require('internal/stream_base_commons');
const {
kReadBytesOrError,
streamBaseState,
} = internalBinding('stream_wrap');
const {
validateNumber,
validateOneOf,
Expand All @@ -60,6 +70,7 @@ const { getOptionValue } = require('internal/options');
const kOnKeylog = Symbol('onkeylog');
const kRequestOptions = Symbol('requestOptions');
const kRequestAsyncResource = Symbol('requestAsyncResource');
const kFreeSocketDataGuard = Symbol('freeSocketDataGuard');

// New Agent code.

Expand Down Expand Up @@ -92,9 +103,51 @@ function freeSocketErrorListener(err) {
// in the TCP buffer and be silently consumed as the response for the
// *next* request that reuses the socket (response-queue poisoning).
// See: https://hackerone.com/reports/3582376
function freeSocketDataGuard() {
debug('DATA on FREE socket - destroying poisoned socket');
this.destroy();
function freeSocketOnReadGuard() {
const nread = streamBaseState[kReadBytesOrError];
if (nread === 0) return;

debug('READ on FREE socket - destroying poisoned socket');
this[owner_symbol].destroy();
}

function installFreeSocketDataGuard(socket) {
if (socket.readableLength > 0) {
debug('BUFFERED DATA on FREE socket - destroying poisoned socket');
socket.destroy();
return;
}

if (socket.connecting) {
socket[kFreeSocketDataGuard] = function onConnect() {
socket[kFreeSocketDataGuard] = null;
installFreeSocketDataGuard(socket);
};
socket.once('connect', socket[kFreeSocketDataGuard]);
return;
}

const handle = socket._handle;
if (handle) {
handle.onread = freeSocketOnReadGuard;
if (!handle.reading) {
handle.reading = true;
const err = handle.readStart();
if (err) socket.destroy();
}
}
}

function removeFreeSocketDataGuard(socket) {
if (socket[kFreeSocketDataGuard]) {
socket.removeListener('connect', socket[kFreeSocketDataGuard]);
socket[kFreeSocketDataGuard] = null;
}

const handle = socket._handle;
if (handle?.onread === freeSocketOnReadGuard) {
handle.onread = onStreamRead;
}
}

function Agent(options) {
Expand Down Expand Up @@ -206,8 +259,7 @@ function Agent(options) {
this.removeSocket(socket, options);

socket.once('error', freeSocketErrorListener);
socket.on('data', freeSocketDataGuard);
socket.resume();
installFreeSocketDataGuard(socket);
freeSockets.push(socket);
});

Expand Down Expand Up @@ -599,7 +651,7 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
Agent.prototype.reuseSocket = function reuseSocket(socket, req) {
debug('have free socket');
socket.removeListener('error', freeSocketErrorListener);
socket.removeListener('data', freeSocketDataGuard);
removeFreeSocketDataGuard(socket);
req.reusedSocket = true;
socket.ref();
};
Expand Down
2 changes: 1 addition & 1 deletion src/node_version.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
#define NODE_VERSION_IS_LTS 1
#define NODE_VERSION_LTS_CODENAME "Jod"

#define NODE_VERSION_IS_RELEASE 0
#define NODE_VERSION_IS_RELEASE 1

#ifndef NODE_STRINGIFY
#define NODE_STRINGIFY(n) NODE_STRINGIFY_HELPER(n)
Expand Down
9 changes: 5 additions & 4 deletions test/parallel/test-http-agent-free-socket-data-guard.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
// writes a full HTTP response during this window, it is consumed as the
// response for the *next* request — poisoning the response queue.
//
// The fix attaches a data guard listener + resume() on idle sockets so
// that unsolicited data causes the socket to be destroyed.
// The fix installs a read guard on idle sockets so that unsolicited data
// causes the socket to be destroyed without adding public stream listeners.

const common = require('../common');
const assert = require('assert');
Expand Down Expand Up @@ -48,8 +48,9 @@ server.listen(0, common.mustCall(() => {
assert.strictEqual(agent.freeSockets[name]?.length, 1);
const freeSocket = agent.freeSockets[name][0];
assert.strictEqual(freeSocket.parser, null);
// With the fix, a data guard listener is attached
assert.strictEqual(freeSocket.listenerCount('data'), 1);
// With the fix, no public stream listeners are added.
assert.strictEqual(freeSocket.listenerCount('data'), 0);
assert.strictEqual(freeSocket.listenerCount('readable'), 0);

// Step 2: Server injects a poisoned response while socket is idle
serverSocket.write(
Expand Down
5 changes: 3 additions & 2 deletions test/parallel/test-http-agent-keepalive.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,9 @@ server.listen(0, common.mustCall(() => {
function checkListeners(socket) {
const callback = common.mustCall(() => {
if (!socket.destroyed) {
// Sockets have freeSocketDataGuard while in the free pool.
assert.strictEqual(socket.listenerCount('data'), 1);
// Sockets have no public stream guard listeners while in the free pool.
assert.strictEqual(socket.listenerCount('readable'), 0);
assert.strictEqual(socket.listenerCount('data'), 0);
assert.strictEqual(socket.listenerCount('drain'), 0);
// Sockets have freeSocketErrorListener.
assert.strictEqual(socket.listenerCount('error'), 1);
Expand Down
Loading