Skip to content

Security: losefor/pg-explain

Security

SECURITY.md

Security Policy

Supported versions

pgexplain is pre-1.0. Security fixes are applied to the latest released minor version. Please always run the most recent 0.x release.

Version Supported
0.3.x
< 0.3

Reporting a vulnerability

Please do not open a public issue for security vulnerabilities.

Report privately via GitHub Security Advisories. Include:

  • a description of the issue and its impact,
  • steps to reproduce (a minimal plan, command, or config is ideal),
  • the pgexplain version (pg-explain --version) and your environment.

We will acknowledge your report, work with you on a fix, and credit you in the release notes if you wish. Please give us a reasonable opportunity to address the issue before any public disclosure.

Security posture

pgexplain is designed to be safe to point at production databases. What it does — and, just as important, where the limits are:

Query execution (run)

  • EXPLAIN runs inside a rolled-back transaction (BEGIN … ROLLBACK) with SET LOCAL transaction_read_only = on, statement_timeout, and lock_timeout, so nothing is committed and runaway queries are bounded.
  • Data-modifying statements are refused by default. Because EXPLAIN ANALYZE actually executes the query, a non-SELECT (INSERT/UPDATE/DELETE/MERGE/DDL) is refused unless you pass --force — and even then it still runs inside the auto-rolled-back transaction.
  • Known limits — read before using --force:
    • ROLLBACK undoes row changes but not every side effect: sequence advances (nextval), NOTIFY, and anything that leaves the database (dblink, postgres_fdw, extensions that touch files or the network) are not rolled back.
    • The read-only check is a keyword heuristic. A SELECT my_function_that_writes() passes as read-only; the transaction rollback still reverts its row changes, but the external side effects above would not be. Prefer a least-privilege, read-only database role as the real guardrail.

Studio (local web UI)

  • Loopback-only by default. Binding a non-loopback host requires an explicit --unsafe-host opt-in.
  • Per-session bearer token. Every /api request must carry a random token minted at startup and delivered only via the URL the CLI prints/opens. Other local processes and web pages cannot call the API without it.
  • Host and Origin validation. Requests with a non-loopback Host (DNS rebinding) or a cross-origin Origin (CSRF) are rejected.
  • Saved passwords go to the OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service) when the optional @napi-rs/keyring module is usable; existing file-stored passwords are migrated on startup. Where no keychain is available, they fall back to ~/.pgexplain/ with owner-only permissions (0700 dir, 0600 database — the same contract libpq enforces for ~/.pgpass). Storing a password is opt-in, it is never returned by the API, and PGPASSWORD / ~/.pgpass work without storing anything. PGEXPLAIN_NO_KEYCHAIN=1 forces file storage.
  • Security headers (strict CSP, nosniff, frame-ancestors 'none', no-store on API responses) are set on every studio response.

Output

  • Credential scrubbing is applied to reports, error messages, and debug output (connection URLs, password=, PGPASSWORD=). It is pattern-based and best-effort — review output before sharing it outside your team.
  • --redact strips string/numeric literals from plan expressions so plans can be shared without leaking predicate values. It is also pattern-based and best-effort, and it is off by default.
  • No telemetry. pgexplain never phones home; it makes no network calls except the PostgreSQL connection you explicitly ask for.

If you believe any of these protections can be bypassed, that is a security issue — please report it via the process above.

There aren't any published security advisories