amqplib 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Change log for amqplib
2
2
 
3
+ ## v2.3.0
4
+ - Reject a `connection.tune` whose `frameMax` is below the protocol minimum of 4096 (the spec allows 0 or at least frame-min-size). Previously such a value was accepted, and the first publish whose encoded frames total 2048 bytes or more entered a loop in `sendContent` that never advanced, exhausting memory. `sendContent` now also throws if `frameMax` leaves no room for a body frame (GHSA-37f9-7m79-6866, reported by Rizwan Bedekar ([@r-bedekar](https://github.com/r-bedekar)); fix based in part on their patch and tests)
5
+ - Add a `maxFrameSize` connection option, also settable as a URL query parameter, bounding the payload size of frames accepted from the server. The default is 64 MiB, or the negotiated `frameMax` if that is larger, so raising `frameMax` needs no second option; `0` disables the limit. Incoming frame sizes were previously unchecked, so a frame header declaring up to 4 GiB made the client buffer, and repeatedly re-copy, that much data before looking at it. An oversized frame is now refused from its header alone and closes the connection with `Frame size N exceeds maxFrameSize M`. The limit is deliberately separate from the negotiated `frameMax`, since some brokers send larger frames without splitting them (see #784). **Upgrade note:** a broker that delivers an unsplit frame larger than the limit will now have that connection closed; pass `maxFrameSize: 0`, or a higher value, to restore the previous behaviour (GHSA-37f9-7m79-6866)
6
+
3
7
  ## v2.2.1
4
8
  - Return the server acknowledgement from promise-based `channel.cancel`, matching the callback API (thanks [@edsonsims](https://github.com/edsonsims)).
5
9
  - Type `cancel` replies as `Replies.Cancel` (`{ consumerTag }`) in both the promise and callback API declarations, which previously claimed `Replies.Empty`.
package/lib/connect.js CHANGED
@@ -47,6 +47,11 @@ function openFrames(vhost, query, credentials, extraClientProperties) {
47
47
  return val === undefined ? def : parseInt(val, 10);
48
48
  }
49
49
 
50
+ function maxFrameSizeOrUndefined(val) {
51
+ if (val == null) return undefined;
52
+ return /^\d+$/.test(String(val)) ? Number(val) : NaN;
53
+ }
54
+
50
55
  const clientProperties = Object.create(CLIENT_PROPERTIES);
51
56
 
52
57
  return {
@@ -61,6 +66,8 @@ function openFrames(vhost, query, credentials, extraClientProperties) {
61
66
  frameMax: intOrDefault(query.frameMax, 131072),
62
67
  heartbeat: query.heartbeat != null ? intOrDefault(query.heartbeat, 0) : null,
63
68
 
69
+ maxFrameSize: maxFrameSizeOrUndefined(query.maxFrameSize),
70
+
64
71
  // open
65
72
  virtualHost: vhost,
66
73
  capabilities: '',
@@ -120,6 +127,7 @@ function connect(url, socketOptions, openCallback) {
120
127
  channelMax: url.channelMax,
121
128
  frameMax: url.frameMax,
122
129
  heartbeat: url.heartbeat,
130
+ maxFrameSize: url.maxFrameSize,
123
131
  };
124
132
 
125
133
  fields = openFrames(url.vhost, config, sockopts.credentials || credentials.plain(user, pass), extraClientProperties);
package/lib/connection.js CHANGED
@@ -27,6 +27,7 @@ const DEFAULT_WRITE_HWM = 1024;
27
27
  // at once. Note that this is less than the minimum frame size: if it
28
28
  // was greater, we might have to fragment the content.
29
29
  const SINGLE_CHUNK_THRESHOLD = 2048;
30
+ const DEFAULT_MAX_FRAME_SIZE = 64 * 1024 * 1024;
30
31
 
31
32
  class Connection extends EventEmitter {
32
33
  constructor(underlying) {
@@ -38,6 +39,7 @@ class Connection extends EventEmitter {
38
39
  // frames
39
40
  this.rest = Buffer.alloc(0);
40
41
  this.frameMax = constants.FRAME_MIN_SIZE;
42
+ this.maxFrameSize = DEFAULT_MAX_FRAME_SIZE;
41
43
  this.sentSinceLastCheck = false;
42
44
  this.recvSinceLastCheck = false;
43
45
 
@@ -86,6 +88,17 @@ class Connection extends EventEmitter {
86
88
  const self = this;
87
89
  const openCallback = openCallback0 || (() => {});
88
90
 
91
+ const maxFrameSize = allFields.maxFrameSize;
92
+ const maxFrameSizeDefaulted = maxFrameSize === undefined || maxFrameSize === null;
93
+ if (maxFrameSizeDefaulted) {
94
+ self.maxFrameSize = DEFAULT_MAX_FRAME_SIZE;
95
+ } else if (!Number.isInteger(maxFrameSize) || (maxFrameSize !== 0 && maxFrameSize < constants.FRAME_MIN_SIZE)) {
96
+ bail(new TypeError(fmt('maxFrameSize must be 0 (no limit) or an integer of at least %d; got %s', constants.FRAME_MIN_SIZE, maxFrameSize)));
97
+ return;
98
+ } else {
99
+ self.maxFrameSize = maxFrameSize;
100
+ }
101
+
89
102
  // This is where we'll put our negotiated values
90
103
  const tunedOptions = Object.create(allFields);
91
104
 
@@ -157,6 +170,10 @@ class Connection extends EventEmitter {
157
170
  break;
158
171
  case defs.ConnectionTune: {
159
172
  const fields = reply.fields;
173
+ if (fields.frameMax !== 0 && fields.frameMax < constants.FRAME_MIN_SIZE) {
174
+ bail(new Error(fmt('Server frameMax %d is below the protocol minimum %d', fields.frameMax, constants.FRAME_MIN_SIZE)));
175
+ return;
176
+ }
160
177
  tunedOptions.frameMax = negotiate(fields.frameMax, allFields.frameMax);
161
178
  tunedOptions.channelMax = negotiate(fields.channelMax, allFields.channelMax);
162
179
  if (allFields.heartbeat == null) {
@@ -191,6 +208,9 @@ class Connection extends EventEmitter {
191
208
  // value is zero, meaning "no, no limits"
192
209
  self.channelMax = tunedOptions.channelMax || 0xffff;
193
210
  self.frameMax = tunedOptions.frameMax || 0xffffffff;
211
+ if (maxFrameSizeDefaulted && tunedOptions.frameMax !== 0 && tunedOptions.frameMax > self.maxFrameSize) {
212
+ self.maxFrameSize = tunedOptions.frameMax;
213
+ }
194
214
  // 0 means "no heartbeat", rather than "maximum period of
195
215
  // heartbeating"
196
216
  self.heartbeat = tunedOptions.heartbeat;
@@ -526,6 +546,9 @@ class Connection extends EventEmitter {
526
546
  const buffer = this.channels[channel].buffer;
527
547
 
528
548
  const maxBody = this.frameMax - FRAME_OVERHEAD;
549
+ if (!(maxBody > 0)) {
550
+ throw new Error(fmt('Cannot send content: frameMax %d leaves no room for a body frame', this.frameMax));
551
+ }
529
552
 
530
553
  for (let offset = 0; offset < body.length; offset += maxBody) {
531
554
  const end = offset + maxBody;
@@ -539,7 +562,7 @@ class Connection extends EventEmitter {
539
562
 
540
563
  recvFrame() {
541
564
  // %%% identifying invariants might help here?
542
- const frame = parseFrame(this.rest);
565
+ const frame = parseFrame(this.rest, this.maxFrameSize);
543
566
 
544
567
  if (!frame) {
545
568
  const incoming = this.stream.read();
@@ -654,3 +677,4 @@ function isFatalError(error) {
654
677
 
655
678
  module.exports.Connection = Connection;
656
679
  module.exports.isFatalError = isFatalError;
680
+ module.exports.DEFAULT_MAX_FRAME_SIZE = DEFAULT_MAX_FRAME_SIZE;
package/lib/frame.js CHANGED
@@ -78,7 +78,7 @@ module.exports.makeBodyFrame = (channel, payload) => {
78
78
  * @arg { number } max
79
79
  * @returns { FrameStructure | boolean }
80
80
  */
81
- function parseFrame(bin) {
81
+ function parseFrame(bin, max) {
82
82
  if (bin.length < FRAME_HEADER_BYTES) {
83
83
  return false;
84
84
  }
@@ -87,6 +87,10 @@ function parseFrame(bin) {
87
87
  const channel = bin.readUInt16BE(1);
88
88
  const size = bin.readUInt32BE(3);
89
89
 
90
+ if (max > 0 && size > max) {
91
+ throw new Error(`Frame size ${size} exceeds maxFrameSize ${max}`);
92
+ }
93
+
90
94
  const totalSize = FRAME_HEADER_BYTES + size + FRAME_END_BYTES;
91
95
 
92
96
  if (bin.length < totalSize) {
@@ -33,6 +33,7 @@ export namespace Options {
33
33
  password?: string;
34
34
  locale?: string;
35
35
  frameMax?: number;
36
+ maxFrameSize?: number;
36
37
  heartbeat?: number;
37
38
  vhost?: string;
38
39
  channelMax?: number;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "amqplib",
3
3
  "homepage": "http://amqp-node.github.io/amqplib/",
4
4
  "main": "./channel_api.js",
5
- "version": "2.2.1",
5
+ "version": "2.3.0",
6
6
  "types": "./index.d.ts",
7
7
  "exports": {
8
8
  ".": {