| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 61b20f6 commit 44c8ebc
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -43,12 +43,22 @@ const { | |||
| 43 | 43 | getGlobalAgent, | |
| 44 | 44 | } = require('internal/http'); | |
| 45 | 45 | const { AsyncResource } = require('async_hooks'); | |
| 46 | - const { async_id_symbol } = require('internal/async_hooks').symbols; | ||
| 46 | + const { | ||
| 47 | + async_id_symbol, | ||
| 48 | + owner_symbol, | ||
| 49 | + } = require('internal/async_hooks').symbols; | ||
| 47 | 50 | const { | |
| 48 | 51 | getLazy, | |
| 49 | 52 | kEmptyObject, | |
| 50 | 53 | once, | |
| 51 | 54 | } = require('internal/util'); | |
| 55 | + const { | ||
| 56 | + onStreamRead, | ||
| 57 | + } = require('internal/stream_base_commons'); | ||
| 58 | + const { | ||
| 59 | + kReadBytesOrError, | ||
| 60 | + streamBaseState, | ||
| 61 | + } = internalBinding('stream_wrap'); | ||
| 52 | 62 | const { | |
| 53 | 63 | validateNumber, | |
| 54 | 64 | validateOneOf, | |
@@ -60,6 +70,7 @@ const { getOptionValue } = require('internal/options'); | |||
| 60 | 70 | const kOnKeylog = Symbol('onkeylog'); | |
| 61 | 71 | const kRequestOptions = Symbol('requestOptions'); | |
| 62 | 72 | const kRequestAsyncResource = Symbol('requestAsyncResource'); | |
| 73 | + const kFreeSocketDataGuard = Symbol('freeSocketDataGuard'); | ||
| 63 | 74 | ||
| 64 | 75 | // New Agent code. | |
| 65 | 76 | ||
@@ -92,9 +103,51 @@ function freeSocketErrorListener(err) { | |||
| 92 | 103 | // in the TCP buffer and be silently consumed as the response for the | |
| 93 | 104 | // *next* request that reuses the socket (response-queue poisoning). | |
| 94 | 105 | // See: https://hackerone.com/reports/3582376 | |
| 95 | - function freeSocketDataGuard() { | ||
| 96 | - debug('DATA on FREE socket - destroying poisoned socket'); | ||
| 97 | - this.destroy(); | ||
| 106 | + function freeSocketOnReadGuard() { | ||
| 107 | + const nread = streamBaseState[kReadBytesOrError]; | ||
| 108 | + if (nread === 0) return; | ||
| 109 | + | ||
| 110 | + debug('READ on FREE socket - destroying poisoned socket'); | ||
| 111 | + this[owner_symbol].destroy(); | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + function installFreeSocketDataGuard(socket) { | ||
| 115 | + if (socket.readableLength > 0) { | ||
| 116 | + debug('BUFFERED DATA on FREE socket - destroying poisoned socket'); | ||
| 117 | + socket.destroy(); | ||
| 118 | + return; | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + if (socket.connecting) { | ||
| 122 | + socket[kFreeSocketDataGuard] = function onConnect() { | ||
| 123 | + socket[kFreeSocketDataGuard] = null; | ||
| 124 | + installFreeSocketDataGuard(socket); | ||
| 125 | + }; | ||
| 126 | + socket.once('connect', socket[kFreeSocketDataGuard]); | ||
| 127 | + return; | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + const handle = socket._handle; | ||
| 131 | + if (handle) { | ||
| 132 | + handle.onread = freeSocketOnReadGuard; | ||
| 133 | + if (!handle.reading) { | ||
| 134 | + handle.reading = true; | ||
| 135 | + const err = handle.readStart(); | ||
| 136 | + if (err) socket.destroy(); | ||
| 137 | + } | ||
| 138 | + } | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + function removeFreeSocketDataGuard(socket) { | ||
| 142 | + if (socket[kFreeSocketDataGuard]) { | ||
| 143 | + socket.removeListener('connect', socket[kFreeSocketDataGuard]); | ||
| 144 | + socket[kFreeSocketDataGuard] = null; | ||
| 145 | + } | ||
| 146 | + | ||
| 147 | + const handle = socket._handle; | ||
| 148 | + if (handle?.onread === freeSocketOnReadGuard) { | ||
| 149 | + handle.onread = onStreamRead; | ||
| 150 | + } | ||
| 98 | 151 | } | |
| 99 | 152 | ||
| 100 | 153 | function Agent(options) { | |
@@ -207,8 +260,7 @@ function Agent(options) { | |||
| 207 | 260 | this.removeSocket(socket, options); | |
| 208 | 261 | ||
| 209 | 262 | socket.once('error', freeSocketErrorListener); | |
| 210 | - socket.on('data', freeSocketDataGuard); | ||
| 211 | - socket.resume(); | ||
| 263 | + installFreeSocketDataGuard(socket); | ||
| 212 | 264 | freeSockets.push(socket); | |
| 213 | 265 | }); | |
| 214 | 266 | ||
@@ -600,7 +652,7 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) { | |||
| 600 | 652 | Agent.prototype.reuseSocket = function reuseSocket(socket, req) { | |
| 601 | 653 | debug('have free socket'); | |
| 602 | 654 | socket.removeListener('error', freeSocketErrorListener); | |
| 603 | - socket.removeListener('data', freeSocketDataGuard); | ||
| 655 | + removeFreeSocketDataGuard(socket); | ||
| 604 | 656 | req.reusedSocket = true; | |
| 605 | 657 | socket.ref(); | |
| 606 | 658 | }; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -8,8 +8,8 @@ | |||
| 8 | 8 | // writes a full HTTP response during this window, it is consumed as the | |
| 9 | 9 | // response for the *next* request — poisoning the response queue. | |
| 10 | 10 | // | |
| 11 | - // The fix attaches a data guard listener + resume() on idle sockets so | ||
| 12 | - // that unsolicited data causes the socket to be destroyed. | ||
| 11 | + // The fix installs a read guard on idle sockets so that unsolicited data | ||
| 12 | + // causes the socket to be destroyed without adding public stream listeners. | ||
| 13 | 13 | ||
| 14 | 14 | const common = require('../common'); | |
| 15 | 15 | const assert = require('assert'); | |
@@ -48,8 +48,9 @@ server.listen(0, common.mustCall(() => { | |||
| 48 | 48 | assert.strictEqual(agent.freeSockets[name]?.length, 1); | |
| 49 | 49 | const freeSocket = agent.freeSockets[name][0]; | |
| 50 | 50 | assert.strictEqual(freeSocket.parser, null); | |
| 51 | - // With the fix, a data guard listener is attached | ||
| 52 | - assert.strictEqual(freeSocket.listenerCount('data'), 1); | ||
| 51 | + // With the fix, no public stream listeners are added. | ||
| 52 | + assert.strictEqual(freeSocket.listenerCount('data'), 0); | ||
| 53 | + assert.strictEqual(freeSocket.listenerCount('readable'), 0); | ||
| 53 | 54 | ||
| 54 | 55 | // Step 2: Server injects a poisoned response while socket is idle | |
| 55 | 56 | serverSocket.write( | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -149,8 +149,9 @@ server.listen(0, common.mustCall(() => { | |||
| 149 | 149 | function checkListeners(socket) { | |
| 150 | 150 | const callback = common.mustCall(() => { | |
| 151 | 151 | if (!socket.destroyed) { | |
| 152 | - // Sockets have freeSocketDataGuard while in the free pool. | ||
| 153 | - assert.strictEqual(socket.listenerCount('data'), 1); | ||
| 152 | + // Sockets have no public stream guard listeners while in the free pool. | ||
| 153 | + assert.strictEqual(socket.listenerCount('readable'), 0); | ||
| 154 | + assert.strictEqual(socket.listenerCount('data'), 0); | ||
| 154 | 155 | assert.strictEqual(socket.listenerCount('drain'), 0); | |
| 155 | 156 | // Sockets have freeSocketErrorListener. | |
| 156 | 157 | assert.strictEqual(socket.listenerCount('error'), 1); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments