Injection

Injection in Node.js Express for CVE-2026-40906 [CVE-2026-40906]

[Fixed month year] Updated CVE-2026-40906

Overview

CVE-2026-40906 describes a dangerous flaw in Electric's Postgres sync engine where the order_by parameter of the /v1/shape API could be exploited to perform error-based SQL injection. Between 1.1.12 and before 1.5.0, authenticated users could craft ORDER BY expressions that altered the query semantics and caused the database to reveal, modify, or even destroy data. This is categorized under CWE-89: Improper Neutralization of Special Elements used in an SQL Command. In practical terms for a Node.js/Express app, similar injection surfaces appear when you directly interpolate untrusted input into an ORDER BY clause, allowing attackers to influence SQL execution and potentially exfiltrate data or disrupt operations. The fix in the affected project is to upgrade to 1.5.0, but the broader remediation for Node.js/Express is to sanitize and constrain ORDER BY inputs, rather than concatenating user input into SQL strings.

Affected Versions

Electric's ElectricSQL /v1/shape API: 1.1.12 up to but not including 1.5.0 (i.e., 1.1.12 ≤ v < 1.5.0)

Code Fix Example

Node.js (Express) API Security Remediation
Vulnerable pattern:
const express = require('express');
const { Pool } = require('pg');
const app = express();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

app.get('/shape', async (req, res) => {
  // Untrusted input used to build ORDER BY
  const order_by = req.query.order_by || 'id ASC';
  const sql = `SELECT id, name, created_at FROM shapes ORDER BY ${order_by} LIMIT 100`;
  try {
    const { rows } = await pool.query(sql);
    res.json(rows);
  } catch (err) {
    res.status(500).send('Query error');
  }
});

Fixed pattern (safer approach using whitelisting and safe formatting):
const express = require('express');
const { Pool } = require('pg');
const format = require('pg-format'); // npm i pg-format
const app = express();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

app.get('/shape', async (req, res) => {
  const allowedColumns = new Set(['id', 'name', 'created_at']);
  const defaultCol = 'id';
  const defaultDir = 'ASC';

  // Extract potential input
  const raw = (req.query.order_by || '').trim(); // expected like: "name DESC" or "created_at ASC"
  let col = defaultCol;
  let dir = defaultDir;

  if (raw) {
    const parts = raw.split(/\s+/);
    if (parts[0] && allowedColumns.has(parts[0])) {
      col = parts[0];
    }
    if (parts[1] && ['ASC', 'DESC'].includes(parts[1].toUpperCase())) {
      dir = parts[1].toUpperCase();
    }
  }

  // Use pg-format to safely interpolate identifiers and directions
  const sql = format('SELECT id, name, created_at FROM shapes ORDER BY %I %s LIMIT 100', col, dir);
  try {
    const { rows } = await pool.query(sql);
    res.json(rows);
  } catch (err) {
    res.status(500).send('Query error');
  }
});

CVE References

Choose which optional cookies to allow. You can change this any time.