Skip to content

Commit

Permalink
Add option to force use of Extended Queries (#3214)
Browse files Browse the repository at this point in the history
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 <alxndrsn>
Co-authored-by: Brian Carlson <[email protected]>
  • Loading branch information
alxndrsn and brianc authored Jun 4, 2024
1 parent fe88e82 commit ff47a97
Show file tree
Hide file tree
Showing 5 changed files with 37 additions and 1 deletion.
3 changes: 3 additions & 0 deletions docs/pages/apis/client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ type QueryConfig {

// custom type parsers just for this query result
types?: Types;

// TODO: document
queryMode?: string;
}
```

Expand Down
2 changes: 1 addition & 1 deletion packages/pg-native/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions packages/pg/lib/native/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
}
Expand Down
5 changes: 5 additions & 0 deletions packages/pg/lib/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || ''
Expand All @@ -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
Expand Down
25 changes: 25 additions & 0 deletions packages/pg/test/integration/client/multiple-results-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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* () {
Expand Down

0 comments on commit ff47a97

Please sign in to comment.