Overview
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
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');
}
});