Repository: elastic/elasticsearch-js
Pull request #1361 (2020-11-24 08:48:40)

[Backport 7.x] Throw if the content length is too big
Backport 502509cabc7ad58b714ea8e025d41fb249b6e12e from #1355

elasticmachine: Since this is a community submitted pull request, a Jenkins build has not been kicked off automatically. Can an Elastic organization member please verify the contents of this patch and then kick off a build manually?


Patch:
diff --git a/lib/Transport.js b/lib/Transport.js
index b5bdcba5..6b0e17ef 100644
--- a/lib/Transport.js
+++ b/lib/Transport.js
@@ -22,6 +22,7 @@
 const debug = require('debug')('elasticsearch')
 const os = require('os')
 const { gzip, unzip, createGzip } = require('zlib')
+const buffer = require('buffer')
 const ms = require('ms')
 const {
   ConnectionError,
@@ -35,6 +36,8 @@ const noop = () => {}
 
 const clientVersion = require('../package.json').version
 const userAgent = `elasticsearch-js/${clientVersion} (${os.platform()} ${os.release()}-${os.arch()}; Node.js ${process.version})`
+const MAX_BUFFER_LENGTH = buffer.constants.MAX_LENGTH
+const MAX_STRING_LENGTH = buffer.constants.MAX_STRING_LENGTH
 
 class Transport {
   constructor (opts) {
@@ -218,6 +221,22 @@ class Transport {
 
       const contentEncoding = (result.headers['content-encoding'] || '').toLowerCase()
       const isCompressed = contentEncoding.indexOf('gzip') > -1 || contentEncoding.indexOf('deflate') > -1
+
+      /* istanbul ignore else */
+      if (result.headers['content-length'] !== undefined) {
+        const contentLength = Number(result.headers['content-length'])
+        if (isCompressed && contentLength > MAX_BUFFER_LENGTH) {
+          response.destroy()
+          return onConnectionError(
+            new RequestAbortedError(`The content length (${contentLength}) is bigger than the maximum allowed buffer (${MAX_BUFFER_LENGTH})`, result)
+          )
+        } else if (contentLength > MAX_STRING_LENGTH) {
+          response.destroy()
+          return onConnectionError(
+            new RequestAbortedError(`The content length (${contentLength}) is bigger than the maximum allowed string (${MAX_STRING_LENGTH})`, result)
+          )
+        }
+      }
       // if the response is compressed, we must handle it
       // as buffer for allowing decompression later
       let payload = isCompressed ? [] : ''


Test patch:
diff --git a/test/unit/client.test.js b/test/unit/client.test.js
index 29993b2d..4c3ae492 100644
--- a/test/unit/client.test.js
+++ b/test/unit/client.test.js
@@ -21,7 +21,9 @@
 
 const { test } = require('tap')
 const { URL } = require('url')
-const { Client, ConnectionPool, Transport, errors } = require('../../index')
+const buffer = require('buffer')
+const intoStream = require('into-stream')
+const { Client, ConnectionPool, Transport, Connection, errors } = require('../../index')
 const { CloudConnectionPool } = require('../../lib/pool')
 const { buildServer } = require('../utils')
 
@@ -1243,3 +1245,58 @@ test('Socket destryed while reading the body', t => {
     })
   })
 })
+
+test('Content length too big (buffer)', t => {
+  t.plan(4)
+
+  class MockConnection extends Connection {
+    request (params, callback) {
+      const stream = intoStream(JSON.stringify({ hello: 'world' }))
+      stream.statusCode = 200
+      stream.headers = {
+        'content-type': 'application/json;utf=8',
+        'content-encoding': 'gzip',
+        'content-length': buffer.constants.MAX_LENGTH + 10,
+        connection: 'keep-alive',
+        date: new Date().toISOString()
+      }
+      stream.on('close', () => t.pass('Stream destroyed'))
+      process.nextTick(callback, null, stream)
+      return { abort () {} }
+    }
+  }
+
+  const client = new Client({ node: 'http://localhost:9200', Connection: MockConnection })
+  client.info((err, result) => {
+    t.ok(err instanceof errors.RequestAbortedError)
+    t.is(err.message, `The content length (${buffer.constants.MAX_LENGTH + 10}) is bigger than the maximum allowed buffer (${buffer.constants.MAX_LENGTH})`)
+    t.strictEqual(result.meta.attempts, 0)
+  })
+})
+
+test('Content length too big (string)', t => {
+  t.plan(4)
+
+  class MockConnection extends Connection {
+    request (params, callback) {
+      const stream = intoStream(JSON.stringify({ hello: 'world' }))
+      stream.statusCode = 200
+      stream.headers = {
+        'content-type': 'application/json;utf=8',
+        'content-length': buffer.constants.MAX_STRING_LENGTH + 10,
+        connection: 'keep-alive',
+        date: new Date().toISOString()
+      }
+      stream.on('close', () => t.pass('Stream destroyed'))
+      process.nextTick(callback, null, stream)
+      return { abort () {} }
+    }
+  }
+
+  const client = new Client({ node: 'http://localhost:9200', Connection: MockConnection })
+  client.info((err, result) => {
+    t.ok(err instanceof errors.RequestAbortedError)
+    t.is(err.message, `The content length (${buffer.constants.MAX_STRING_LENGTH + 10}) is bigger than the maximum allowed string (${buffer.constants.MAX_STRING_LENGTH})`)
+    t.strictEqual(result.meta.attempts, 0)
+  })
+})