Skip to main content
interlace
Plugin: pgRules

no-transaction-on-pool

Prevents running transaction commands directly on pool (must use dedicated client).

CWE: CWE-693

⚠️ This rule errors by default in the recommended config.

Quick Summary

AspectDetails
CWE ReferenceCWE-362 (Race Condition)
SeverityHigh (CVSS: 7.5)
CategorySecurity

Rule Details

pool.query() acquires a different client for each call. Transaction commands (BEGIN, COMMIT, ROLLBACK) on pool may execute on different connections, breaking transaction semantics.

❌ Incorrect

// Each query might use a different connection!
await pool.query('BEGIN');
await pool.query('INSERT INTO users VALUES ($1)', [user]);
await pool.query('COMMIT'); // May commit nothing

✅ Correct

const client = await pool.connect();
try {
  await client.query('BEGIN');
  await client.query('INSERT INTO users VALUES ($1)', [user]);
  await client.query('COMMIT');
} catch (e) {
  await client.query('ROLLBACK');
  throw e;
} finally {
  client.release();
}

Error Message Format

⚠️ CWE-362 | Transaction command on pool causes race condition | HIGH
   Fix: Use pool.connect() and run transaction commands on the returned client

When Not To Use It

  • Never disable this rule - transactions on pool are always incorrect

Known False Negatives

The following patterns are not detected due to static analysis limitations:

Aliased and imported pools (detected since 2026-10)

The receiver is followed to where it is created — through const db = pool, and through a relative import into the module that writes new Pool() (or declares the handle with pg's Pool type). An imported new Client() is a single connection, where a transaction is correct, and is not reported.

// ❌ REPORTED - ./db exports `new Pool()`
import { pool } from './db';
await pool.query('BEGIN');

Dynamic Query String

Why: Transaction commands in variables are not detected.

// ❌ NOT DETECTED - Query from variable
const cmd = 'BEGIN';
await pool.query(cmd);

Mitigation: Use literal query strings for transaction commands.

Wrapped Pool Methods

Why: Wrapper functions hide pool access.

// ❌ NOT DETECTED - Transaction in wrapper
async function runQuery(sql: string) {
  return pool.query(sql);
}
await runQuery('BEGIN'); // Wrapper not traced

Mitigation: Apply rule to all modules. Use dedicated transaction wrappers that enforce client pattern.

Transaction Commands in Template Literals

Why: Complex template literals may hide transaction keywords.

// ❌ NOT DETECTED - Hidden in template
const action = 'BEGIN';
await pool.query(`${action}`);

Mitigation: Use explicit transaction management patterns. Create utility functions that enforce correct usage.

Use this rule

npm install -D oxlint-plugin-postgresql-security
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-postgresql-security"],  "rules": {    "postgresql-security/no-transaction-on-pool": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-postgresql-security'],    rules: {      'postgresql-security/no-transaction-on-pool': 'error',    },  },});

oxlint-plugin-postgresql-security re-exports eslint-plugin-postgresql-security/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.