Skip to content

[Security] Prototype pollution via server-supplied column names in result rows #3654

@eddieran

Description

@eddieran

Split from #3651 as suggested by @charmander.

Summary

When a PostgreSQL server returns column names like __proto__, constructor, or toString, these are used as object keys in result rows without any sanitization. A rogue or compromised PostgreSQL server can exploit this to pollute Object.prototype on the client.

Affected code

packages/pg/lib/result.js — both addFields() and parseRow():

// addFields() builds the pre-built empty result object:
const row = {}
for (let i = 0; i < fieldDescriptions.length; i++) {
  const desc = fieldDescriptions[i]
  row[desc.name] = null  // desc.name comes from server
}
this._prebuiltEmptyResultObject = { ...row }

// parseRow() assigns parsed values using server-supplied field names:
parseRow(rowData) {
  const row = { ...this._prebuiltEmptyResultObject }
  for (let i = 0, len = rowData.length; i < len; i++) {
    const field = this.fields[i].name  // server-controlled
    row[field] = this._parsers[i](v)   // value also server-controlled
  }
  return row
}

Threat model

This is a rogue server scenario, not a trusted-config issue. If a client connects to a malicious PostgreSQL server (e.g. via DNS hijack, MITM without TLS verification, or a compromised host), the server controls the column names in RowDescription messages. A column named __proto__ with a crafted value can pollute the prototype chain of all objects in the Node.js process.

Steps to reproduce

  1. Set up a mock PostgreSQL server that returns a RowDescription with a field named __proto__ and a DataRow with the value {"polluted": true} (as a JSON/text column)
  2. Connect a pg client to this server and execute any query
  3. After the result is parsed, check ({}).polluted — it will be true

Impact

  • Denial of service (crash or undefined behavior in downstream code)
  • Potential RCE depending on how the application uses objects after pollution

Suggested fix

Use Object.create(null) instead of {} for result rows, or reject/rename column names that collide with Object.prototype properties:

const row = Object.create(null)

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions