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 | ❌ |
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.
pgexplain is designed to be safe to point at production databases. What it does — and, just as important, where the limits are:
- EXPLAIN runs inside a rolled-back transaction (
BEGIN … ROLLBACK) withSET LOCAL transaction_read_only = on,statement_timeout, andlock_timeout, so nothing is committed and runaway queries are bounded. - Data-modifying statements are refused by default. Because
EXPLAIN ANALYZEactually 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:ROLLBACKundoes 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.
- Loopback-only by default. Binding a non-loopback host requires an
explicit
--unsafe-hostopt-in. - Per-session bearer token. Every
/apirequest 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-originOrigin(CSRF) are rejected. - Saved passwords go to the OS keychain (macOS Keychain, Windows
Credential Manager, Linux Secret Service) when the optional
@napi-rs/keyringmodule is usable; existing file-stored passwords are migrated on startup. Where no keychain is available, they fall back to~/.pgexplain/with owner-only permissions (0700dir,0600database — the same contract libpq enforces for~/.pgpass). Storing a password is opt-in, it is never returned by the API, andPGPASSWORD/~/.pgpasswork without storing anything.PGEXPLAIN_NO_KEYCHAIN=1forces file storage. - Security headers (strict CSP,
nosniff,frame-ancestors 'none',no-storeon API responses) are set on every studio response.
- 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. --redactstrips 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.