From ff47a97f280b697234654e9982501c2c055302b9 Mon Sep 17 00:00:00 2001 From: Alex Anderson <191496+alxndrsn@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:14:04 +0100 Subject: [PATCH] Add option to force use of Extended Queries (#3214) This feature can be used as follows: ``` client.query({ text: 'SELECT 1', queryMode: 'extended' }) ``` This will force the query to be sent with parse/bind/execute even when it has no parameters and disallows multiple statements being executed. This can be useful in scenarios where you want to enforce more security & help prevent sql injection attacks...particularly by library authors. --------- Co-authored-by: alxndrsn Co-authored-by: Brian Carlson --- docs/pages/apis/client.mdx | 3 +++ packages/pg-native/index.js | 2 +- packages/pg/lib/native/query.js | 3 +++ packages/pg/lib/query.js | 5 ++++ .../client/multiple-results-tests.js | 25 +++++++++++++++++++ 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/pages/apis/client.mdx b/docs/pages/apis/client.mdx index 8175448e2..648136139 100644 --- a/docs/pages/apis/client.mdx +++ b/docs/pages/apis/client.mdx @@ -75,6 +75,9 @@ type QueryConfig { // custom type parsers just for this query result types?: Types; + + // TODO: document + queryMode?: string; } ``` diff --git a/packages/pg-native/index.js b/packages/pg-native/index.js index 87980197c..a3c3f070b 100644 --- a/packages/pg-native/index.js +++ b/packages/pg-native/index.js @@ -57,7 +57,7 @@ Client.prototype.query = function (text, values, cb) { cb = values } - if (Array.isArray(values) && values.length > 0) { + if (Array.isArray(values)) { queryFn = function () { return self.pq.sendQueryParams(text, values) } diff --git a/packages/pg/lib/native/query.js b/packages/pg/lib/native/query.js index d06db43ca..0cfed1fda 100644 --- a/packages/pg/lib/native/query.js +++ b/packages/pg/lib/native/query.js @@ -10,6 +10,7 @@ var NativeQuery = (module.exports = function (config, values, callback) { this.text = config.text this.values = config.values this.name = config.name + this.queryMode = config.queryMode this.callback = config.callback this.state = 'new' this._arrayMode = config.rowMode === 'array' @@ -159,6 +160,8 @@ NativeQuery.prototype.submit = function (client) { } var vals = this.values.map(utils.prepareValue) client.native.query(this.text, vals, after) + } else if (this.queryMode === 'extended') { + client.native.query(this.text, [], after) } else { client.native.query(this.text, after) } diff --git a/packages/pg/lib/query.js b/packages/pg/lib/query.js index fac4d86e3..0925960e6 100644 --- a/packages/pg/lib/query.js +++ b/packages/pg/lib/query.js @@ -16,6 +16,7 @@ class Query extends EventEmitter { this.rows = config.rows this.types = config.types this.name = config.name + this.queryMode = config.queryMode this.binary = config.binary // use unique portal name each time this.portal = config.portal || '' @@ -32,6 +33,10 @@ class Query extends EventEmitter { } requiresPreparation() { + if (this.queryMode === 'extended') { + return true + } + // named queries must always be prepared if (this.name) { return true diff --git a/packages/pg/test/integration/client/multiple-results-tests.js b/packages/pg/test/integration/client/multiple-results-tests.js index addca9b68..c27295103 100644 --- a/packages/pg/test/integration/client/multiple-results-tests.js +++ b/packages/pg/test/integration/client/multiple-results-tests.js @@ -25,6 +25,31 @@ suite.test( }) ) +suite.test( + 'throws if queryMode set to "extended"', + co.wrap(function* () { + const client = new helper.Client() + yield client.connect() + + // TODO should be text or sql? + try { + const results = yield client.query({ + text: `SELECT 'foo'::text as name; SELECT 'bar'::text as baz`, + queryMode: 'extended', + }) + assert.fail('Should have thrown') + } catch (err) { + if (err instanceof assert.AssertionError) throw err + + assert.equal(err.severity, 'ERROR') + assert.equal(err.code, '42601') + assert.equal(err.message, 'cannot insert multiple commands into a prepared statement') + } + + return client.end() + }) +) + suite.test( 'multiple selects work', co.wrap(function* () {