diff --git a/packages/pg-cursor/index.js b/packages/pg-cursor/index.js index 5d566882b..31a140e47 100644 --- a/packages/pg-cursor/index.js +++ b/packages/pg-cursor/index.js @@ -6,6 +6,7 @@ const EventEmitter = require('events').EventEmitter const util = require('util') let nextUniqueID = 1 // concept borrowed from org.postgresql.core.v3.QueryExecutorImpl +const maxParameterCount = 0xffff class Cursor extends EventEmitter { constructor(text, values, config) { @@ -43,6 +44,12 @@ class Cursor extends EventEmitter { } submit(connection) { + if (this.values && this.values.length > maxParameterCount) { + return new RangeError( + `PostgreSQL supports a maximum of ${maxParameterCount} parameters, but ${this.values.length} were supplied` + ) + } + this.state = 'submitted' this.connection = connection this._portal = 'C_' + nextUniqueID++ diff --git a/packages/pg-cursor/test/parameter-count.js b/packages/pg-cursor/test/parameter-count.js new file mode 100644 index 000000000..c0eacb543 --- /dev/null +++ b/packages/pg-cursor/test/parameter-count.js @@ -0,0 +1,50 @@ +'use strict' +const assert = require('assert') +const EventEmitter = require('events') +const Cursor = require('../') + +const maxParameterCount = 0xffff +const errorMessage = `PostgreSQL supports a maximum of ${maxParameterCount} parameters, but ${ + maxParameterCount + 1 +} were supplied` + +const createConnection = function () { + const connection = new EventEmitter() + connection.calls = { parse: 0, bind: 0, describe: 0, flush: 0 } + connection.parse = function () { + connection.calls.parse++ + } + connection.bind = function () { + connection.calls.bind++ + } + connection.describe = function () { + connection.calls.describe++ + } + connection.flush = function () { + connection.calls.flush++ + } + return connection +} + +describe('parameter count limits', function () { + it('allows the maximum parameter count', function () { + const connection = createConnection() + const cursor = new Cursor('select $1', new Array(maxParameterCount).fill(null)) + + const err = cursor.submit(connection) + + assert.equal(err, undefined) + assert.deepStrictEqual(connection.calls, { parse: 1, bind: 1, describe: 1, flush: 1 }) + }) + + it('rejects too many parameters before sending query messages', function () { + const connection = createConnection() + const cursor = new Cursor('select $1', new Array(maxParameterCount + 1).fill(null)) + + const err = cursor.submit(connection) + + assert(err instanceof RangeError) + assert.equal(err.message, errorMessage) + assert.deepStrictEqual(connection.calls, { parse: 0, bind: 0, describe: 0, flush: 0 }) + }) +}) diff --git a/packages/pg-protocol/src/outbound-serializer.test.ts b/packages/pg-protocol/src/outbound-serializer.test.ts index aac0c57ba..ee6f63f95 100644 --- a/packages/pg-protocol/src/outbound-serializer.test.ts +++ b/packages/pg-protocol/src/outbound-serializer.test.ts @@ -153,6 +153,55 @@ describe('serializer', () => { }) }) + describe('parameter count limits', function () { + const maxParameterCount = 0xffff + const errorMessage = `PostgreSQL supports a maximum of ${maxParameterCount} parameters, but ${ + maxParameterCount + 1 + } were supplied` + + it('supports the maximum parameter count in a bind message', function () { + const actual = serialize.bind({ values: new Array(maxParameterCount).fill(null) }) + const valueCountOffset = 9 + maxParameterCount * 2 + + assert.equal(actual.readUInt16BE(7), maxParameterCount) + assert.equal(actual.readUInt16BE(valueCountOffset), maxParameterCount) + }) + + it('rejects too many bind parameters before mapping values', function () { + let mapped = false + + assert.throws( + () => + serialize.bind({ + values: new Array(maxParameterCount + 1), + valueMapper: () => { + mapped = true + return null + }, + }), + { name: 'RangeError', message: errorMessage } + ) + assert.equal(mapped, false) + + const actual = serialize.query('select 1') + const expected = new BufferList().addCString('select 1').join(true, 'Q') + assert.deepEqual(actual, expected) + }) + + it('supports the maximum parameter type count in a parse message', function () { + const actual = serialize.parse({ text: '', types: new Array(maxParameterCount).fill(0) }) + + assert.equal(actual.readUInt16BE(7), maxParameterCount) + }) + + it('rejects too many parameter types in a parse message', function () { + assert.throws(() => serialize.parse({ text: '', types: new Array(maxParameterCount + 1) }), { + name: 'RangeError', + message: errorMessage, + }) + }) + }) + it('with custom valueMapper', function () { const actual = serialize.bind({ portal: 'bang', diff --git a/packages/pg-protocol/src/serializer.ts b/packages/pg-protocol/src/serializer.ts index bbd59623e..b6d52991b 100644 --- a/packages/pg-protocol/src/serializer.ts +++ b/packages/pg-protocol/src/serializer.ts @@ -17,6 +17,13 @@ const enum code { } const writer = new Writer() +const maxParameterCount = 0xffff + +const checkParameterCount = (count: number): void => { + if (count > maxParameterCount) { + throw new RangeError(`PostgreSQL supports a maximum of ${maxParameterCount} parameters, but ${count} were supplied`) + } +} const startup = (opts: Record): Buffer => { // protocol version @@ -86,6 +93,7 @@ const parse = (query: ParseOpts): Buffer => { const types = query.types || emptyArray const len = types.length + checkParameterCount(len) const buffer = writer .addCString(name) // name of query @@ -150,6 +158,7 @@ const bind = (config: BindOpts = {}): Buffer => { const binary = config.binary || false const values = config.values || emptyArray const len = values.length + checkParameterCount(len) writer.addCString(portal).addCString(statement) diff --git a/packages/pg/lib/query.js b/packages/pg/lib/query.js index 3b93cb5ba..c8d439e20 100644 --- a/packages/pg/lib/query.js +++ b/packages/pg/lib/query.js @@ -5,6 +5,8 @@ const { EventEmitter } = require('events') const Result = require('./result') const utils = require('./utils') +const maxParameterCount = 0xffff + class Query extends EventEmitter { constructor(config, values, callback) { super() @@ -161,6 +163,11 @@ class Query extends EventEmitter { return new Error('Query values must be an array') } if (this.requiresPreparation()) { + if (this.values && this.values.length > maxParameterCount) { + return new RangeError( + `PostgreSQL supports a maximum of ${maxParameterCount} parameters, but ${this.values.length} were supplied` + ) + } // If we're using the extended query protocol we fire off several separate commands // to the backend. On some versions of node & some operating system versions // the network stack writes each message separately instead of buffering them together diff --git a/packages/pg/test/integration/gh-issues/2579-tests.js b/packages/pg/test/integration/gh-issues/2579-tests.js new file mode 100644 index 000000000..36eca9f66 --- /dev/null +++ b/packages/pg/test/integration/gh-issues/2579-tests.js @@ -0,0 +1,55 @@ +'use strict' +const assert = require('assert') +const helper = require('../test-helper') + +const suite = new helper.Suite() +const maxParameterCount = 0xffff +const errorMessage = `PostgreSQL supports a maximum of ${maxParameterCount} parameters, but ${ + maxParameterCount + 1 +} were supplied` + +suite.test( + 'rejects too many parameters and keeps the connection usable', + !helper.args.native && + async function () { + const client = new helper.Client() + await client.connect() + try { + await assert.rejects(client.query({ text: 'select $1', values: new Array(maxParameterCount + 1) }), { + name: 'RangeError', + message: errorMessage, + }) + + const result = await client.query('select 1 as value') + assert.equal(result.rows[0].value, 1) + } finally { + await client.end() + } + } +) + +suite.test( + 'isolates an oversized query in pipeline mode', + !helper.args.native && + async function () { + const client = new helper.Client({ pipeline: true }) + await client.connect() + try { + const results = await Promise.allSettled([ + client.query('select 1 as value'), + client.query({ text: 'select $1', values: new Array(maxParameterCount + 1) }), + client.query('select 2 as value'), + ]) + + assert.equal(results[0].status, 'fulfilled') + assert.equal(results[0].value.rows[0].value, 1) + assert.equal(results[1].status, 'rejected') + assert(results[1].reason instanceof RangeError) + assert.equal(results[1].reason.message, errorMessage) + assert.equal(results[2].status, 'fulfilled') + assert.equal(results[2].value.rows[0].value, 2) + } finally { + await client.end() + } + } +) diff --git a/packages/pg/test/unit/client/parameter-count-tests.js b/packages/pg/test/unit/client/parameter-count-tests.js new file mode 100644 index 000000000..cceb08faa --- /dev/null +++ b/packages/pg/test/unit/client/parameter-count-tests.js @@ -0,0 +1,86 @@ +'use strict' +const assert = require('assert') +const helper = require('./test-helper') +const Query = require('../../../lib/query') + +const suite = new helper.Suite() +const maxParameterCount = 0xffff +const errorMessage = `PostgreSQL supports a maximum of ${maxParameterCount} parameters, but ${ + maxParameterCount + 1 +} were supplied` + +suite.test('allows the maximum parameter count', function () { + const client = helper.client() + const query = new Query({ + text: 'select $1', + values: new Array(maxParameterCount).fill(null), + }) + + const err = query.submit(client.connection) + + assert.equal(err, null) + assert.equal(client.connection.parseMessages.length, 1) + assert.equal(client.connection.bindMessages.length, 1) + assert.equal(client.connection.syncCount, 1) +}) + +suite.test('rejects too many parameters before sending query messages', function (done) { + const client = helper.client() + const con = client.connection + let callbackCalls = 0 + let queryReturned = false + + con.emit('readyForQuery') + client.query( + { + text: 'select $1', + values: new Array(maxParameterCount + 1), + }, + function (err) { + callbackCalls++ + assert.equal(queryReturned, true, 'the callback must be asynchronous') + assert(err instanceof RangeError) + assert.equal(err.message, errorMessage) + assert.equal(con.parseMessages.length, 0) + assert.equal(con.bindMessages.length, 0) + assert.equal(con.syncCount, 0) + + client.query('select 1', function (err) { + assert.ifError(err) + assert.equal(callbackCalls, 1) + done() + }) + process.nextTick(function () { + con.emit('readyForQuery') + }) + } + ) + queryReturned = true +}) + +suite.test('isolates an oversized query in pipeline mode', async function () { + const client = helper.client({ pipeline: true }) + const con = client.connection + con.emit('readyForQuery') + + const queries = [ + client.query('select 1'), + client.query({ text: 'select $1', values: new Array(maxParameterCount + 1) }), + client.query('select 2'), + ] + + assert.deepStrictEqual(con.queries, ['select 1', 'select 2']) + assert.equal(con.parseMessages.length, 0) + assert.equal(con.bindMessages.length, 0) + assert.equal(con.syncCount, 0) + + con.emit('readyForQuery') + con.emit('readyForQuery') + + const results = await Promise.allSettled(queries) + assert.equal(results[0].status, 'fulfilled') + assert.equal(results[1].status, 'rejected') + assert(results[1].reason instanceof RangeError) + assert.equal(results[1].reason.message, errorMessage) + assert.equal(results[2].status, 'fulfilled') +})