diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3d15e48b47155..8888942914eef1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,7 +33,8 @@ release.
12.0.0
+10.17.0
10.16.2
10.16.1
10.16.0
@@ -54,6 +55,50 @@
* [io.js](CHANGELOG_IOJS.md)
* [Archive](CHANGELOG_ARCHIVE.md)
+
+## 2019-08-15, Version 10.17.0 'Dubnium' (LTS), @BethGriggs
+
+### Notable changes
+
+This is a security release.
+
+Node.js, as well as many other implementations of HTTP/2, have been found
+vulnerable to Denial of Service attacks.
+See https://github.com/Netflix/security-bulletins/blob/master/advisories/third-party/2019-002.md
+for more information.
+
+Vulnerabilities fixed:
+
+* **CVE-2019-9511 “Data Dribble”**: The attacker requests a large amount of data from a specified resource over multiple streams. They manipulate window size and stream priority to force the server to queue the data in 1-byte chunks. Depending on how efficiently this data is queued, this can consume excess CPU, memory, or both, potentially leading to a denial of service.
+* **CVE-2019-9512 “Ping Flood”**: The attacker sends continual pings to an HTTP/2 peer, causing the peer to build an internal queue of responses. Depending on how efficiently this data is queued, this can consume excess CPU, memory, or both, potentially leading to a denial of service.
+* **CVE-2019-9513 “Resource Loop”**: The attacker creates multiple request streams and continually shuffles the priority of the streams in a way that causes substantial churn to the priority tree. This can consume excess CPU, potentially leading to a denial of service.
+* **CVE-2019-9514 “Reset Flood”**: The attacker opens a number of streams and sends an invalid request over each stream that should solicit a stream of RST_STREAM frames from the peer. Depending on how the peer queues the RST_STREAM frames, this can consume excess memory, CPU, or both, potentially leading to a denial of service.
+* **CVE-2019-9515 “Settings Flood”**: The attacker sends a stream of SETTINGS frames to the peer. Since the RFC requires that the peer reply with one acknowledgement per SETTINGS frame, an empty SETTINGS frame is almost equivalent in behavior to a ping. Depending on how efficiently this data is queued, this can consume excess CPU, memory, or both, potentially leading to a denial of service.
+* **CVE-2019-9516 “0-Length Headers Leak”**: The attacker sends a stream of headers with a 0-length header name and 0-length header value, optionally Huffman encoded into 1-byte or greater headers. Some implementations allocate memory for these headers and keep the allocation alive until the session dies. This can consume excess memory, potentially leading to a denial of service.
+* **CVE-2019-9517 “Internal Data Buffering”**: The attacker opens the HTTP/2 window so the peer can send without constraint; however, they leave the TCP window closed so the peer cannot actually write (many of) the bytes on the wire. The attacker then sends a stream of requests for a large response object. Depending on how the servers queue the responses, this can consume excess memory, CPU, or both, potentially leading to a denial of service.
+* **CVE-2019-9518 “Empty Frames Flood”**: The attacker sends a stream of frames with an empty payload and without the end-of-stream flag. These frames can be DATA, HEADERS, CONTINUATION and/or PUSH_PROMISE. The peer spends time processing each frame disproportionate to attack bandwidth. This can consume excess CPU, potentially leading to a denial of service. (Discovered by Piotr Sikora of Google)
+
+### Commits
+
+* [[`74507fae34`](https://github.com/nodejs/node/commit/74507fae34)] - **deps**: update nghttp2 to 1.39.2 (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`a397c881ec`](https://github.com/nodejs/node/commit/a397c881ec)] - **deps**: update nghttp2 to 1.39.1 (gengjiawen) [#28448](https://github.com/nodejs/node/pull/28448)
+* [[`fedfa12a33`](https://github.com/nodejs/node/commit/fedfa12a33)] - **deps**: update nghttp2 to 1.38.0 (gengjiawen) [#27295](https://github.com/nodejs/node/pull/27295)
+* [[`ab0f2ace36`](https://github.com/nodejs/node/commit/ab0f2ace36)] - **(SEMVER-MINOR)** **deps**: update nghttp2 to 1.37.0 (gengjiawen) [#26990](https://github.com/nodejs/node/pull/26990)
+* [[`0acbe05ee2`](https://github.com/nodejs/node/commit/0acbe05ee2)] - **http2**: allow security revert for Ping/Settings Flood (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`c152449012`](https://github.com/nodejs/node/commit/c152449012)] - **http2**: pause input processing if sending output (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`0ce699c7b1`](https://github.com/nodejs/node/commit/0ce699c7b1)] - **http2**: stop reading from socket if writes are in progress (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`17357d37a9`](https://github.com/nodejs/node/commit/17357d37a9)] - **http2**: consider 0-length non-end DATA frames an error (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`460f896c63`](https://github.com/nodejs/node/commit/460f896c63)] - **http2**: shrink default `vector::reserve()` allocations (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`f4242e24f9`](https://github.com/nodejs/node/commit/f4242e24f9)] - **http2**: handle 0-length headers better (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`477461a51f`](https://github.com/nodejs/node/commit/477461a51f)] - **http2**: limit number of invalid incoming frames (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`05dada46ee`](https://github.com/nodejs/node/commit/05dada46ee)] - **http2**: limit number of rejected stream openings (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`7f11465572`](https://github.com/nodejs/node/commit/7f11465572)] - **http2**: do not create ArrayBuffers when no DATA received (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`2eb914ff5f`](https://github.com/nodejs/node/commit/2eb914ff5f)] - **http2**: only call into JS when necessary for session events (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`76a7ada15d`](https://github.com/nodejs/node/commit/76a7ada15d)] - **http2**: improve JS-side debug logging (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+* [[`00f153da13`](https://github.com/nodejs/node/commit/00f153da13)] - **http2**: improve http2 code a bit (James M Snell) [#23984](https://github.com/nodejs/node/pull/23984)
+* [[`a0a14c809f`](https://github.com/nodejs/node/commit/a0a14c809f)] - **src**: pass along errors from http2 object creation (Anna Henningsen) [#25822](https://github.com/nodejs/node/pull/25822)
+* [[`d85e4006ab`](https://github.com/nodejs/node/commit/d85e4006ab)] - **test**: apply test-http2-max-session-memory-leak from v12.x (Anna Henningsen) [#29122](https://github.com/nodejs/node/pull/29122)
+
## 2019-08-06, Version 10.16.2 'Dubnium' (LTS), @BethGriggs
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index a57e63c235398a..d2f1c22ac705df 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -127,6 +127,26 @@ const { StreamPipe } = internalBinding('stream_pipe');
const { _connectionListener: httpConnectionListener } = http;
const debug = util.debuglog('http2');
+// TODO(addaleax): See if this can be made more efficient by figuring out
+// whether debugging is enabled before we perform any further steps. Currently,
+// this seems pretty fast, though.
+function debugStream(id, sessionType, message, ...args) {
+ debug('Http2Stream %s [Http2Session %s]: ' + message,
+ id, sessionName(sessionType), ...args);
+}
+
+function debugStreamObj(stream, message, ...args) {
+ debugStream(stream[kID], stream[kSession][kType], ...args);
+}
+
+function debugSession(sessionType, message, ...args) {
+ debug('Http2Session %s: ' + message, sessionName(sessionType), ...args);
+}
+
+function debugSessionObj(session, message, ...args) {
+ debugSession(session[kType], message, ...args);
+}
+
const kMaxFrameSize = (2 ** 24) - 1;
const kMaxInt = (2 ** 32) - 1;
const kMaxStreams = (2 ** 31) - 1;
@@ -147,6 +167,7 @@ const kID = Symbol('id');
const kInit = Symbol('init');
const kInfoHeaders = Symbol('sent-info-headers');
const kLocalSettings = Symbol('local-settings');
+const kNativeFields = Symbol('kNativeFields');
const kOptions = Symbol('options');
const kOwner = owner_symbol;
const kOrigin = Symbol('origin');
@@ -168,7 +189,15 @@ const {
paddingBuffer,
PADDING_BUF_FRAME_LENGTH,
PADDING_BUF_MAX_PAYLOAD_LENGTH,
- PADDING_BUF_RETURN_VALUE
+ PADDING_BUF_RETURN_VALUE,
+ kBitfield,
+ kSessionPriorityListenerCount,
+ kSessionFrameErrorListenerCount,
+ kSessionUint8FieldCount,
+ kSessionHasRemoteSettingsListeners,
+ kSessionRemoteSettingsIsUpToDate,
+ kSessionHasPingListeners,
+ kSessionHasAltsvcListeners,
} = binding;
const {
@@ -248,8 +277,7 @@ function onSessionHeaders(handle, id, cat, flags, headers) {
const type = session[kType];
session[kUpdateTimer]();
- debug(`Http2Stream ${id} [Http2Session ` +
- `${sessionName(type)}]: headers received`);
+ debugStream(id, type, 'headers received');
const streams = session[kState].streams;
const endOfStream = !!(flags & NGHTTP2_FLAG_END_STREAM);
@@ -309,8 +337,7 @@ function onSessionHeaders(handle, id, cat, flags, headers) {
const originSet = session[kState].originSet = initOriginSet(session);
originSet.delete(stream[kOrigin]);
}
- debug(`Http2Stream ${id} [Http2Session ` +
- `${sessionName(type)}]: emitting stream '${event}' event`);
+ debugStream(id, type, "emitting stream '%s' event", event);
process.nextTick(emit, stream, event, obj, flags, headers);
}
if (endOfStream) {
@@ -346,12 +373,82 @@ function submitRstStream(code) {
}
}
+// Keep track of the number/presence of JS event listeners. Knowing that there
+// are no listeners allows the C++ code to skip calling into JS for an event.
+function sessionListenerAdded(name) {
+ switch (name) {
+ case 'ping':
+ this[kNativeFields][kBitfield] |= 1 << kSessionHasPingListeners;
+ break;
+ case 'altsvc':
+ this[kNativeFields][kBitfield] |= 1 << kSessionHasAltsvcListeners;
+ break;
+ case 'remoteSettings':
+ this[kNativeFields][kBitfield] |= 1 << kSessionHasRemoteSettingsListeners;
+ break;
+ case 'priority':
+ this[kNativeFields][kSessionPriorityListenerCount]++;
+ break;
+ case 'frameError':
+ this[kNativeFields][kSessionFrameErrorListenerCount]++;
+ break;
+ }
+}
+
+function sessionListenerRemoved(name) {
+ switch (name) {
+ case 'ping':
+ if (this.listenerCount(name) > 0) return;
+ this[kNativeFields][kBitfield] &= ~(1 << kSessionHasPingListeners);
+ break;
+ case 'altsvc':
+ if (this.listenerCount(name) > 0) return;
+ this[kNativeFields][kBitfield] &= ~(1 << kSessionHasAltsvcListeners);
+ break;
+ case 'remoteSettings':
+ if (this.listenerCount(name) > 0) return;
+ this[kNativeFields][kBitfield] &=
+ ~(1 << kSessionHasRemoteSettingsListeners);
+ break;
+ case 'priority':
+ this[kNativeFields][kSessionPriorityListenerCount]--;
+ break;
+ case 'frameError':
+ this[kNativeFields][kSessionFrameErrorListenerCount]--;
+ break;
+ }
+}
+
+// Also keep track of listeners for the Http2Stream instances, as some events
+// are emitted on those objects.
+function streamListenerAdded(name) {
+ switch (name) {
+ case 'priority':
+ this[kSession][kNativeFields][kSessionPriorityListenerCount]++;
+ break;
+ case 'frameError':
+ this[kSession][kNativeFields][kSessionFrameErrorListenerCount]++;
+ break;
+ }
+}
+
+function streamListenerRemoved(name) {
+ switch (name) {
+ case 'priority':
+ this[kSession][kNativeFields][kSessionPriorityListenerCount]--;
+ break;
+ case 'frameError':
+ this[kSession][kNativeFields][kSessionFrameErrorListenerCount]--;
+ break;
+ }
+}
+
function onPing(payload) {
const session = this[kOwner];
if (session.destroyed)
return;
session[kUpdateTimer]();
- debug(`Http2Session ${sessionName(session[kType])}: new ping received`);
+ debugSessionObj(session, 'new ping received');
session.emit('ping', payload);
}
@@ -366,8 +463,7 @@ function onStreamClose(code) {
if (stream.destroyed)
return;
- debug(`Http2Stream ${stream[kID]} [Http2Session ` +
- `${sessionName(stream[kSession][kType])}]: closed with code ${code}`);
+ debugStreamObj(stream, 'closed with code %d', code);
if (!stream.closed)
closeStream(stream, code, kNoRstStream);
@@ -403,8 +499,7 @@ function onSettings() {
if (session.destroyed)
return;
session[kUpdateTimer]();
- debug(`Http2Session ${sessionName(session[kType])}: new settings received`);
- session[kRemoteSettings] = undefined;
+ debugSessionObj(session, 'new settings received');
session.emit('remoteSettings', session.remoteSettings);
}
@@ -415,9 +510,9 @@ function onPriority(id, parent, weight, exclusive) {
const session = this[kOwner];
if (session.destroyed)
return;
- debug(`Http2Stream ${id} [Http2Session ` +
- `${sessionName(session[kType])}]: priority [parent: ${parent}, ` +
- `weight: ${weight}, exclusive: ${exclusive}]`);
+ debugStream(id, session[kType],
+ 'priority [parent: %d, weight: %d, exclusive: %s]',
+ parent, weight, exclusive);
const emitter = session[kState].streams.get(id) || session;
if (!emitter.destroyed) {
emitter[kUpdateTimer]();
@@ -431,8 +526,8 @@ function onFrameError(id, type, code) {
const session = this[kOwner];
if (session.destroyed)
return;
- debug(`Http2Session ${sessionName(session[kType])}: error sending frame ` +
- `type ${type} on stream ${id}, code: ${code}`);
+ debugSessionObj(session, 'error sending frame type %d on stream %d, code: %d',
+ type, id, code);
const emitter = session[kState].streams.get(id) || session;
emitter[kUpdateTimer]();
emitter.emit('frameError', type, code, id);
@@ -442,8 +537,8 @@ function onAltSvc(stream, origin, alt) {
const session = this[kOwner];
if (session.destroyed)
return;
- debug(`Http2Session ${sessionName(session[kType])}: altsvc received: ` +
- `stream: ${stream}, origin: ${origin}, alt: ${alt}`);
+ debugSessionObj(session, 'altsvc received: stream: %d, origin: %s, alt: %s',
+ stream, origin, alt);
session[kUpdateTimer]();
session.emit('altsvc', alt, origin, stream);
}
@@ -470,8 +565,7 @@ function onOrigin(origins) {
const session = this[kOwner];
if (session.destroyed)
return;
- debug('Http2Session %s: origin received: %j',
- sessionName(session[kType]), origins);
+ debugSessionObj(session, 'origin received: %j', origins);
session[kUpdateTimer]();
if (!session.encrypted || session.destroyed)
return undefined;
@@ -491,8 +585,8 @@ function onGoawayData(code, lastStreamID, buf) {
const session = this[kOwner];
if (session.destroyed)
return;
- debug(`Http2Session ${sessionName(session[kType])}: goaway ${code} ` +
- `received [last stream id: ${lastStreamID}]`);
+ debugSessionObj(session, 'goaway %d received [last stream id: %d]',
+ code, lastStreamID);
const state = session[kState];
state.goawayCode = code;
@@ -545,8 +639,7 @@ function requestOnConnect(headers, options) {
return;
}
- debug(`Http2Session ${sessionName(session[kType])}: connected, ` +
- 'initializing request');
+ debugSessionObj(session, 'connected, initializing request');
let streamOptions = 0;
if (options.endStream)
@@ -641,13 +734,13 @@ function settingsCallback(cb, ack, duration) {
this[kState].pendingAck--;
this[kLocalSettings] = undefined;
if (ack) {
- debug(`Http2Session ${sessionName(this[kType])}: settings received`);
+ debugSessionObj(this, 'settings received');
const settings = this.localSettings;
if (typeof cb === 'function')
cb(null, settings, duration);
this.emit('localSettings', settings);
} else {
- debug(`Http2Session ${sessionName(this[kType])}: settings canceled`);
+ debugSessionObj(this, 'settings canceled');
if (typeof cb === 'function')
cb(new ERR_HTTP2_SETTINGS_CANCEL());
}
@@ -657,7 +750,7 @@ function settingsCallback(cb, ack, duration) {
function submitSettings(settings, callback) {
if (this.destroyed)
return;
- debug(`Http2Session ${sessionName(this[kType])}: submitting settings`);
+ debugSessionObj(this, 'submitting settings');
this[kUpdateTimer]();
updateSettingsBuffer(settings);
if (!this[kHandle].settings(settingsCallback.bind(this, callback))) {
@@ -691,7 +784,7 @@ function submitPriority(options) {
function submitGoaway(code, lastStreamID, opaqueData) {
if (this.destroyed)
return;
- debug(`Http2Session ${sessionName(this[kType])}: submitting goaway`);
+ debugSessionObj(this, 'submitting goaway');
this[kUpdateTimer]();
this[kHandle].goaway(code, lastStreamID, opaqueData);
}
@@ -821,7 +914,9 @@ function setupHandle(socket, type, options) {
process.nextTick(emit, this, 'connect', this, socket);
return;
}
- debug(`Http2Session ${sessionName(type)}: setting up session handle`);
+
+ debugSession(type, 'setting up session handle');
+
this[kState].flags |= SESSION_FLAGS_READY;
updateOptionsBuffer(options);
@@ -846,6 +941,10 @@ function setupHandle(socket, type, options) {
handle.consume(socket._handle._externalStream);
this[kHandle] = handle;
+ if (this[kNativeFields])
+ handle.fields.set(this[kNativeFields]);
+ else
+ this[kNativeFields] = handle.fields;
if (socket.encrypted) {
this[kAlpnProtocol] = socket.alpnProtocol;
@@ -887,6 +986,7 @@ function finishSessionDestroy(session, error) {
session[kProxySocket] = undefined;
session[kSocket] = undefined;
session[kHandle] = undefined;
+ session[kNativeFields] = new Uint8Array(kSessionUint8FieldCount);
socket[kSession] = undefined;
socket[kServer] = undefined;
@@ -965,6 +1065,7 @@ class Http2Session extends EventEmitter {
this[kProxySocket] = null;
this[kSocket] = socket;
this[kTimeout] = null;
+ this[kHandle] = undefined;
// Do not use nagle's algorithm
if (typeof socket.setNoDelay === 'function')
@@ -983,7 +1084,12 @@ class Http2Session extends EventEmitter {
setupFn();
}
- debug(`Http2Session ${sessionName(type)}: created`);
+ if (!this[kNativeFields])
+ this[kNativeFields] = new Uint8Array(kSessionUint8FieldCount);
+ this.on('newListener', sessionListenerAdded);
+ this.on('removeListener', sessionListenerRemoved);
+
+ debugSession(type, 'created');
}
// Returns undefined if the socket is not yet connected, true if the
@@ -1137,13 +1243,18 @@ class Http2Session extends EventEmitter {
// The settings currently in effect for the remote peer.
get remoteSettings() {
- const settings = this[kRemoteSettings];
- if (settings !== undefined)
- return settings;
+ if (this[kNativeFields][kBitfield] &
+ (1 << kSessionRemoteSettingsIsUpToDate)) {
+ const settings = this[kRemoteSettings];
+ if (settings !== undefined) {
+ return settings;
+ }
+ }
if (this.destroyed || this.connecting)
return {};
+ this[kNativeFields][kBitfield] |= (1 << kSessionRemoteSettingsIsUpToDate);
return this[kRemoteSettings] = getSettings(this[kHandle], true); // Remote
}
@@ -1156,7 +1267,7 @@ class Http2Session extends EventEmitter {
if (callback && typeof callback !== 'function')
throw new ERR_INVALID_CALLBACK();
- debug(`Http2Session ${sessionName(this[kType])}: sending settings`);
+ debugSessionObj(this, 'sending settings');
this[kState].pendingAck++;
@@ -1197,7 +1308,7 @@ class Http2Session extends EventEmitter {
destroy(error = NGHTTP2_NO_ERROR, code) {
if (this.destroyed)
return;
- debug(`Http2Session ${sessionName(this[kType])}: destroying`);
+ debugSessionObj(this, 'destroying');
if (typeof error === 'number') {
code = error;
@@ -1258,7 +1369,7 @@ class Http2Session extends EventEmitter {
close(callback) {
if (this.closed || this.destroyed)
return;
- debug(`Http2Session ${sessionName(this[kType])}: marking session closed`);
+ debugSessionObj(this, 'marking session closed');
this[kState].flags |= SESSION_FLAGS_CLOSED;
if (typeof callback === 'function')
this.once('close', callback);
@@ -1326,6 +1437,12 @@ class ServerHttp2Session extends Http2Session {
constructor(options, socket, server) {
super(NGHTTP2_SESSION_SERVER, options, socket);
this[kServer] = server;
+ // This is a bit inaccurate because it does not reflect changes to
+ // number of listeners made after the session was created. This should
+ // not be an issue in practice. Additionally, the 'priority' event on
+ // server instances (or any other object) is fully undocumented.
+ this[kNativeFields][kSessionPriorityListenerCount] =
+ server.listenerCount('priority');
}
get server() {
@@ -1430,7 +1547,7 @@ class ClientHttp2Session extends Http2Session {
// Submits a new HTTP2 request to the connected peer. Returns the
// associated Http2Stream instance.
request(headers, options) {
- debug(`Http2Session ${sessionName(this[kType])}: initiating request`);
+ debugSessionObj(this, 'initiating request');
if (this.destroyed)
throw new ERR_HTTP2_INVALID_SESSION();
@@ -1643,6 +1760,9 @@ class Http2Stream extends Duplex {
};
this.on('pause', streamOnPause);
+
+ this.on('newListener', streamListenerAdded);
+ this.on('removeListener', streamListenerRemoved);
}
[kUpdateTimer]() {
@@ -1827,8 +1947,7 @@ class Http2Stream extends Duplex {
if (this.pending) {
this.once('ready', () => this._final(cb));
} else if (handle !== undefined) {
- debug(`Http2Stream ${this[kID]} [Http2Session ` +
- `${sessionName(this[kSession][kType])}]: _final shutting down`);
+ debugStreamObj(this, '_final shutting down');
const req = new ShutdownWrap();
req.oncomplete = afterShutdown;
req.callback = cb;
@@ -1887,9 +2006,7 @@ class Http2Stream extends Duplex {
assertIsObject(headers, 'headers');
headers = Object.assign(Object.create(null), headers);
- const session = this[kSession];
- debug(`Http2Stream ${this[kID]} [Http2Session ` +
- `${sessionName(session[kType])}]: sending trailers`);
+ debugStreamObj(this, 'sending trailers');
this[kUpdateTimer]();
@@ -1944,8 +2061,8 @@ class Http2Stream extends Duplex {
const handle = this[kHandle];
const id = this[kID];
- debug(`Http2Stream ${this[kID] || ''} [Http2Session ` +
- `${sessionName(session[kType])}]: destroying stream`);
+ debugStream(this[kID] || 'pending', session[kType], 'destroying stream');
+
const state = this[kState];
const code = err != null ?
NGHTTP2_INTERNAL_ERROR : (state.rstCode || NGHTTP2_NO_ERROR);
@@ -2256,8 +2373,7 @@ class ServerHttp2Stream extends Http2Stream {
const session = this[kSession];
- debug(`Http2Stream ${this[kID]} [Http2Session ` +
- `${sessionName(session[kType])}]: initiating push stream`);
+ debugStreamObj(this, 'initiating push stream');
this[kUpdateTimer]();
@@ -2339,9 +2455,7 @@ class ServerHttp2Stream extends Http2Stream {
assertIsObject(options, 'options');
options = Object.assign({}, options);
- const session = this[kSession];
- debug(`Http2Stream ${this[kID]} [Http2Session ` +
- `${sessionName(session[kType])}]: initiating response`);
+ debugStreamObj(this, 'initiating response');
this[kUpdateTimer]();
options.endStream = !!options.endStream;
@@ -2420,8 +2534,7 @@ class ServerHttp2Stream extends Http2Stream {
validateNumber(fd, 'fd');
- debug(`Http2Stream ${this[kID]} [Http2Session ` +
- `${sessionName(session[kType])}]: initiating response from fd`);
+ debugStreamObj(this, 'initiating response from fd');
this[kUpdateTimer]();
this.ownsFd = false;
@@ -2481,8 +2594,7 @@ class ServerHttp2Stream extends Http2Stream {
}
const session = this[kSession];
- debug(`Http2Stream ${this[kID]} [Http2Session ` +
- `${sessionName(session[kType])}]: initiating response from file`);
+ debugStreamObj(this, 'initiating response from file');
this[kUpdateTimer]();
this.ownsFd = true;
@@ -2515,9 +2627,7 @@ class ServerHttp2Stream extends Http2Stream {
assertIsObject(headers, 'headers');
headers = Object.assign(Object.create(null), headers);
- const session = this[kSession];
- debug(`Http2Stream ${this[kID]} [Http2Session ` +
- `${sessionName(session[kType])}]: sending additional headers`);
+ debugStreamObj(this, 'sending additional headers');
if (headers[HTTP2_HEADER_STATUS] != null) {
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
@@ -2608,8 +2718,7 @@ function socketOnError(error) {
// we can do and the other side is fully within its rights to do so.
if (error.code === 'ECONNRESET' && session[kState].goawayCode !== null)
return session.destroy();
- debug(`Http2Session ${sessionName(session[kType])}: socket error [` +
- `${error.message}]`);
+ debugSessionObj(this, 'socket error [%s]', error.message);
session.destroy(error);
}
}
@@ -2627,7 +2736,7 @@ function sessionOnPriority(stream, parent, weight, exclusive) {
}
function sessionOnError(error) {
- if (this[kServer])
+ if (this[kServer] !== undefined)
this[kServer].emit('sessionError', error, this);
}
@@ -2654,7 +2763,8 @@ function connectionListener(socket) {
return httpConnectionListener.call(this, socket);
}
// Let event handler deal with the socket
- debug(`Unknown protocol from ${socket.remoteAddress}:${socket.remotePort}`);
+ debug('Unknown protocol from %s:%s',
+ socket.remoteAddress, socket.remotePort);
if (!this.emit('unknownProtocol', socket)) {
// We don't know what to do, so let's just tell the other side what's
// going on in a format that they *might* understand.
@@ -2675,8 +2785,10 @@ function connectionListener(socket) {
const session = new ServerHttp2Session(options, socket, this);
session.on('stream', sessionOnStream);
- session.on('priority', sessionOnPriority);
session.on('error', sessionOnError);
+ // Don't count our own internal listener.
+ session.on('priority', sessionOnPriority);
+ session[kNativeFields][kSessionPriorityListenerCount]--;
if (this.timeout)
session.setTimeout(this.timeout, sessionOnTimeout);
@@ -2779,7 +2891,7 @@ function setupCompat(ev) {
function socketOnClose() {
const session = this[kSession];
if (session !== undefined) {
- debug(`Http2Session ${sessionName(session[kType])}: socket closed`);
+ debugSessionObj(session, 'socket closed');
const err = session.connecting ? new ERR_SOCKET_CLOSED() : null;
const state = session[kState];
state.streams.forEach((stream) => stream.close(NGHTTP2_CANCEL));
diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js
index 94dc1198ea1060..c8701af616f327 100644
--- a/lib/internal/http2/util.js
+++ b/lib/internal/http2/util.js
@@ -430,14 +430,20 @@ function mapToHeaders(map,
let count = 0;
const keys = Object.keys(map);
const singles = new Set();
- for (var i = 0; i < keys.length; i++) {
- let key = keys[i];
- let value = map[key];
+ let i;
+ let isArray;
+ let key;
+ let value;
+ let isSingleValueHeader;
+ let err;
+ for (i = 0; i < keys.length; i++) {
+ key = keys[i];
+ value = map[key];
if (value === undefined || key === '')
continue;
key = key.toLowerCase();
- const isSingleValueHeader = kSingleValueHeaders.has(key);
- let isArray = Array.isArray(value);
+ isSingleValueHeader = kSingleValueHeaders.has(key);
+ isArray = Array.isArray(value);
if (isArray) {
switch (value.length) {
case 0:
@@ -459,26 +465,26 @@ function mapToHeaders(map,
singles.add(key);
}
if (key[0] === ':') {
- const err = assertValuePseudoHeader(key);
+ err = assertValuePseudoHeader(key);
if (err !== undefined)
return err;
ret = `${key}\0${value}\0${ret}`;
count++;
- } else {
- if (isIllegalConnectionSpecificHeader(key, value)) {
- return new ERR_HTTP2_INVALID_CONNECTION_HEADERS(key);
- }
- if (isArray) {
- for (var k = 0; k < value.length; k++) {
- const val = String(value[k]);
- ret += `${key}\0${val}\0`;
- }
- count += value.length;
- } else {
- ret += `${key}\0${value}\0`;
- count++;
+ continue;
+ }
+ if (isIllegalConnectionSpecificHeader(key, value)) {
+ return new ERR_HTTP2_INVALID_CONNECTION_HEADERS(key);
+ }
+ if (isArray) {
+ for (var k = 0; k < value.length; k++) {
+ const val = String(value[k]);
+ ret += `${key}\0${val}\0`;
}
+ count += value.length;
+ continue;
}
+ ret += `${key}\0${value}\0`;
+ count++;
}
return [ret, count];
diff --git a/src/env.h b/src/env.h
index d7003616ece13c..7f77face35057f 100644
--- a/src/env.h
+++ b/src/env.h
@@ -173,6 +173,7 @@ struct PackageConfig {
V(family_string, "family") \
V(fatal_exception_string, "_fatalException") \
V(fd_string, "fd") \
+ V(fields_string, "fields") \
V(file_string, "file") \
V(fingerprint256_string, "fingerprint256") \
V(fingerprint_string, "fingerprint") \
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 6bd282593e28f5..43d1c2ea9f277a 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -6,6 +6,8 @@
#include "node_http2_state.h"
#include "node_internals.h"
#include "node_perf.h"
+#include "node_revert.h"
+#include "util-inl.h"
#include
@@ -24,6 +26,7 @@ using v8::ObjectTemplate;
using v8::String;
using v8::Uint32;
using v8::Uint32Array;
+using v8::Uint8Array;
using v8::Undefined;
using node::performance::PerformanceEntry;
@@ -149,6 +152,9 @@ Http2Options::Http2Options(Environment* env, nghttp2_session_type type) {
buffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS]);
}
+ if (IsReverted(SECURITY_REVERT_CVE_2019_9512))
+ nghttp2_option_set_max_outbound_ack(options_, 10000);
+
// The padding strategy sets the mechanism by which we determine how much
// additional frame padding to apply to DATA and HEADERS frames. Currently
// this is set on a per-session basis, but eventually we may switch to
@@ -228,27 +234,18 @@ void Http2Session::Http2Settings::Init() {
count_ = n;
}
-Http2Session::Http2Settings::Http2Settings(Environment* env,
- Http2Session* session, uint64_t start_time)
- : AsyncWrap(env,
- env->http2settings_constructor_template()
- ->NewInstance(env->context())
- .ToLocalChecked(),
- PROVIDER_HTTP2SETTINGS),
- session_(session),
- startTime_(start_time) {
- Init();
-}
-
-
-Http2Session::Http2Settings::Http2Settings(Environment* env)
- : Http2Settings(env, nullptr, 0) {}
-
// The Http2Settings class is used to configure a SETTINGS frame that is
// to be sent to the connected peer. The settings are set using a TypedArray
// that is shared with the JavaScript side.
-Http2Session::Http2Settings::Http2Settings(Http2Session* session)
- : Http2Settings(session->env(), session, uv_hrtime()) {}
+Http2Session::Http2Settings::Http2Settings(Environment* env,
+ Http2Session* session,
+ Local |