Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/pg-connection-string/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function parse(str, options = {}) {

// Check for empty host in URL

const config = {}
const config = Object.create(null)
let result
let dummyHost = false
if (/ |%[^a-f0-9]|%[a-f0-9][^a-f0-9]/i.test(str)) {
Expand Down Expand Up @@ -164,7 +164,7 @@ function toConnectionOptions(sslConfig) {
}

return c
}, {})
}, Object.create(null))

return connectionOptions
}
Expand Down Expand Up @@ -200,7 +200,7 @@ function toClientConfig(config) {
}

return c
}, {})
}, Object.create(null))

return poolConfig
}
Expand Down
14 changes: 7 additions & 7 deletions packages/pg-connection-string/test/clientConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('toClientConfig', function () {
const config = parse('pg:///?sslmode=no-verify')
const clientConfig = toClientConfig(config)

clientConfig.ssl?.should.deep.equal({
expect(clientConfig.ssl).to.deep.equal({
rejectUnauthorized: false,
})
})
Expand All @@ -55,14 +55,14 @@ describe('toClientConfig', function () {
const config = parse('pg:///?sslmode=verify-ca')
const clientConfig = toClientConfig(config)

clientConfig.ssl?.should.deep.equal({})
expect(clientConfig.ssl).to.deep.equal({})
})

it('converts other sslmode options', function () {
const config = parse('pg:///?sslmode=verify-ca')
const clientConfig = toClientConfig(config)

clientConfig.ssl?.should.deep.equal({})
expect(clientConfig.ssl).to.deep.equal({})
})

it('converts ssl cert options', function () {
Expand All @@ -77,7 +77,7 @@ describe('toClientConfig', function () {
const config = parse(connectionString)
const clientConfig = toClientConfig(config)

clientConfig.ssl?.should.deep.equal({
expect(clientConfig.ssl).to.deep.equal({
ca: 'example ca\n',
cert: 'example cert\n',
key: 'example key\n',
Expand Down Expand Up @@ -106,9 +106,9 @@ describe('toClientConfig', function () {

const clientConfig = toClientConfig(config)

clientConfig.host?.should.equal('boom')
clientConfig.database?.should.equal('lala')
clientConfig.ssl?.should.deep.equal({})
expect(clientConfig.host).to.equal('boom')
expect(clientConfig.database).to.equal('lala')
expect(clientConfig.ssl).to.deep.equal({})
})
})

Expand Down
34 changes: 34 additions & 0 deletions packages/pg-connection-string/test/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,4 +467,38 @@ describe('parse', function () {
const subject = parse(connectionString)
subject.port?.should.equal('1234')
})

describe('prototype pollution protection', function () {
it('returns object with null prototype', function () {
const subject = parse('postgres://localhost/db')
expect(Object.getPrototypeOf(subject)).to.equal(null)
})

it('__proto__ query parameter is stored as regular property', function () {
const subject = parse('postgres://localhost/db?__proto__=malicious')
expect(Object.getPrototypeOf(subject)).to.equal(null)
expect(subject['__proto__']).to.equal('malicious')
// global Object.prototype should not be affected
expect(({} as any).malicious).to.equal(undefined)
})

it('constructor query parameter is stored as regular property', function () {
const subject = parse('postgres://localhost/db?constructor=evil')
expect(subject.constructor).to.equal('evil')
})

it('prototype query parameter is stored as regular property', function () {
const subject = parse('postgres://localhost/db?prototype=evil')
expect(subject['prototype']).to.equal('evil')
})

it('multiple dangerous query parameters are handled safely', function () {
const subject = parse('postgres://localhost/db?__proto__=a&constructor=b&prototype=c&toString=d')
expect(Object.getPrototypeOf(subject)).to.equal(null)
expect(subject['__proto__']).to.equal('a')
expect(subject.constructor).to.equal('b')
expect(subject['prototype']).to.equal('c')
expect(subject['toString']).to.equal('d')
})
})
})
2 changes: 1 addition & 1 deletion packages/pg/lib/result.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ class Result {
this._parsers = new Array(fieldDescriptions.length)
}

const row = {}
const row = Object.create(null)

for (let i = 0; i < fieldDescriptions.length; i++) {
const desc = fieldDescriptions[i]
Expand Down
111 changes: 111 additions & 0 deletions packages/pg/test/unit/result-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
'use strict'
const helper = require('./test-helper')
const assert = require('assert')
const suite = new helper.Suite()
const test = suite.test.bind(suite)

const Result = require('../../lib/result')

test('__proto__ column name does not pollute prototype', function () {
const result = new Result()
result.addFields([
{ name: '__proto__', dataTypeID: 25, format: 'text' },
{ name: 'id', dataTypeID: 23, format: 'text' },
])
const row = result.parseRow(['malicious', '1'])

// __proto__ should be a regular property, not affect prototype chain
assert.strictEqual(row['__proto__'], 'malicious')
assert.strictEqual(row.id, 1)

// global Object.prototype should not be affected
assert.strictEqual({}.malicious, undefined)
assert.strictEqual(Object.prototype.malicious, undefined)
})

test('__proto__ column with object value does not inject prototype', function () {
// custom type parser that returns objects (like JSON)
const customTypes = {
getTypeParser: () => (val) => JSON.parse(val),
}
const result = new Result('object', customTypes)
result.addFields([
{ name: '__proto__', dataTypeID: 114, format: 'text' },
{ name: 'id', dataTypeID: 23, format: 'text' },
])

const maliciousPayload = JSON.stringify({ isAdmin: true, role: 'admin' })
const row = result.parseRow([maliciousPayload, '1'])

// __proto__ should be stored as a regular property
assert.deepStrictEqual(row['__proto__'], { isAdmin: true, role: 'admin' })

// the row should NOT inherit from the malicious payload
assert.strictEqual('isAdmin' in row, false)
assert.strictEqual('role' in row, false)
})

test('constructor column name is safely stored as property', function () {
const result = new Result()
result.addFields([
{ name: 'constructor', dataTypeID: 25, format: 'text' },
{ name: 'id', dataTypeID: 23, format: 'text' },
])
const row = result.parseRow(['malicious', '1'])

assert.strictEqual(row.constructor, 'malicious')
assert.strictEqual(row.id, 1)
})

test('hasOwnProperty column name is safely stored as property', function () {
const result = new Result()
result.addFields([
{ name: 'hasOwnProperty', dataTypeID: 25, format: 'text' },
{ name: 'data', dataTypeID: 25, format: 'text' },
])
const row = result.parseRow(['not_a_function', 'value'])

assert.strictEqual(row.hasOwnProperty, 'not_a_function')
assert.strictEqual(row.data, 'value')

// can still check properties using Object.prototype.hasOwnProperty.call
assert.strictEqual(Object.prototype.hasOwnProperty.call(row, 'data'), true)
})

test('toString column name is safely stored as property', function () {
const result = new Result()
result.addFields([{ name: 'toString', dataTypeID: 25, format: 'text' }])
const row = result.parseRow(['not_a_function'])

assert.strictEqual(row.toString, 'not_a_function')
})

test('prototype column name is safely stored as property', function () {
const result = new Result()
result.addFields([
{ name: 'prototype', dataTypeID: 25, format: 'text' },
{ name: 'id', dataTypeID: 23, format: 'text' },
])
const row = result.parseRow(['value', '1'])

assert.strictEqual(row.prototype, 'value')
assert.strictEqual(row.id, 1)
})

test('multiple dangerous column names handled safely', function () {
const result = new Result()
result.addFields([
{ name: '__proto__', dataTypeID: 25, format: 'text' },
{ name: 'constructor', dataTypeID: 25, format: 'text' },
{ name: 'prototype', dataTypeID: 25, format: 'text' },
{ name: '__defineGetter__', dataTypeID: 25, format: 'text' },
{ name: 'id', dataTypeID: 23, format: 'text' },
])
const row = result.parseRow(['a', 'b', 'c', 'd', '1'])

assert.strictEqual(row['__proto__'], 'a')
assert.strictEqual(row.constructor, 'b')
assert.strictEqual(row.prototype, 'c')
assert.strictEqual(row['__defineGetter__'], 'd')
assert.strictEqual(row.id, 1)
})
Loading