Commit Graph
8233 Commits
Author SHA1 Message Date
Ashesh Vashi f02e9be500 docs(release-notes): backfill 9.16 entries from master commits
Add 14 release-note entries that were merged on master but not yet
captured in the 9.16 notes: 1 new feature (#2431), 6 housekeeping
(#9817, #9866, #9917, #9959, #10014, #10023) and 7 bug fixes (#9701,
#9782, #9933, #9952, #9985, #10013, #10030). Entries inserted in
numeric order within each section.

Also add an "Additional changes (no associated issue) -> Dependencies"
section mirroring the 9.15 format, listing net direct dep bumps
between REL-9_15 and HEAD across Python (requirements.txt,
tools/requirements.txt, web/regression/requirements.txt), web/
package.json, and runtime/package.json. Transitive yarn resolutions
and the setuptools pin (covered by bug fix #9829) are excluded.
2026-06-13 01:11:14 +05:30
Ashesh Vashi 8c4fb7f9f9 Merge remote-tracking branch 'origin/master' into cve-9.16
# Conflicts:
#	docs/en_US/release_notes_9_16.rst
2026-06-13 00:41:26 +05:30
dependabot[bot] 8c5805279d Javascript dependency: Bump shell-quote from 1.8.3 to 1.8.4 in /web (#10079) 2026-06-12 23:23:49 +05:30
Dave Page c33927f55b Add a "Back to login" link to the password pages. #9301 (#10037)
The Forgot Password and Reset Password pages had no way to navigate back
to the login page. Added a "Back to login" link (using the login URL
already used by the login form) to both pages.
2026-06-12 23:07:27 +05:30
Dave Page 94564e406b Fix double dot in generated font and image filenames. #9806 (#10032)
Webpack 5 asset modules include the leading dot in the [ext] token, so
the 'img/[name].[ext]' and 'fonts/[name].[ext]' templates produced
filenames with a double dot (e.g. Roboto-Bold..ttf). Use '[name][ext]'
so the emitted filenames are correct.
2026-06-12 23:05:02 +05:30
Dave Page 7b9b8cdd07 Rebase the version-specific SQL templates to target PostgreSQL 14 (#10054)
The base (default) SQL templates previously targeted PostgreSQL < 12.
Re-base them so the default target is 14 - the oldest supported server
version - by collapsing every version bucket <= 14 (11_plus, 12_plus,
13_plus, 14_plus and the old default) into a single `default`, keeping
per file the content a v14 server resolves today. Buckets for newer
versions (15_plus, 16_plus, 17_plus, 18_plus) are retained as overrides.

The transformation is behaviour-preserving for every server version >= 14:
template (and test-fixture) resolution is byte-identical before and after
for all supported versions, verified programmatically across every bucket
container and confirmed by the resql, ERD and Schema Diff suites against
PostgreSQL 18.

Also drop PostgreSQL/EDB Advanced Server 13 from the 9.16 supported-server
list and repoint the sqleditor explain_plan tests (which referenced the
removed 12_plus/13_plus buckets) at the new default template.

Closes #10050
2026-06-12 23:02:56 +05:30
0b4bb3ea3f Propagate column renames to FK and unique constraints in the table dialog (#9060) (#10046)
* Propagate column renames to FK and unique constraints. #9060

In the new-table dialog, the primary key already updated its column
references when a column was renamed, but foreign key and unique
constraint definitions did not, leaving them pointing at the old name.
Mirror the PK rename-propagation in the foreign_key and unique_constraint
depChange handlers (and add 'columns' to the unique constraint deps so it
fires on column changes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address review feedback for column rename propagation (#9060)

- Remap unique constraint INCLUDE columns on rename. The INCLUDE list
  holds bare name strings (not {column} objects), so renaming an
  included column previously emitted stale DDL. Now mirrors the
  primary key INCLUDE handling.
- Add regression tests covering rename propagation in depChange for
  foreign_key and unique_constraint, including the unique constraint
  INCLUDE case.
- Correct the release note wording from "Create/Edit Table" to
  "Create Table"; the propagation only runs on the new-table path
  (state.oid === undefined).

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Ashesh Vashi <ashesh.vashi@enterprisedb.com>
2026-06-12 23:01:28 +05:30
Dave Page 6de0f371b2 Pin setuptools<82 on Python 3.9 to keep pkg_resources. #9829 (#10045)
setuptools 82 stops shipping pkg_resources, which passlib (via
Flask-Security-Too on Python 3.9) imports, so a clean install on Python
3.9 failed with "No module named 'pkg_resources'". Python 3.9 is still a
supported target (RHEL/Rocky/AlmaLinux 8 and 9 build with system Python
3.9). Split the pin by Python version, mirroring the existing
Flask-Security-Too split.
2026-06-12 22:54:53 +05:30
Dave Page 0649bcdb96 Restore Alt+drag rectangular selection in the editor. #9864 #10029 (#10043)
The fix for #9570 (stop Alt+F5 showing a crosshair cursor) changed the
rectangular-selection eventFilter to require Alt+Ctrl and moved the
crosshair cue to Control. That broke the long-standing Alt+drag block
(column) selection and left an inconsistent Ctrl crosshair cue.

Restore the default rectangularSelection() (Alt+drag) and drop
crosshairCursor entirely - the crosshair-on-Alt was exactly the artifact
#9570 wanted gone, and CodeMirror's crosshair cannot be limited to an
active drag. This brings back block selection (#9864, #10029) while
keeping #9570's intent (no crosshair on the Alt+F5 shortcut).
2026-06-12 22:53:44 +05:30
Dave Page 2a34eda5bc Fix ERD delete crash on stale foreign key column. #8318 (#10042)
removeOneToManyLink looked up a column by the FK's stored local_column
name and read .attnum unconditionally. After a column rename the stored
name no longer matches, so _.find returned undefined and .attnum threw,
blocking deletion of the table/link. Use optional chaining so a stale FK
simply doesn't match the link being removed and deletion proceeds.
2026-06-12 22:50:50 +05:30
Dave Page ba60f0ae63 Apply server foreground colour to object counts (#9766) (#10041)
Apply muted server foreground colour to column type labels (#9766)

Issue #9766 asked for both the object counts and the column type text
to follow the server's custom foreground colour. The dynamic per-server
CSS rule only recoloured the file-name and children-count spans, leaving
the column type text (span.text-muted) at its default low-contrast
colour.

Recolour span.text-muted to a reduced-emphasis blend of the foreground
colour using color-mix, so the datatype still reads as de-emphasised
secondary text while following the server colour.
2026-06-12 22:49:46 +05:30
Dave Page 9939d335e0 Let clicks pass through the object breadcrumbs popup. #9128 (#10038)
The breadcrumbs popup is an absolutely-positioned, informational overlay
at the bottom-left of the object explorer, so it intercepted pointer
events and blocked clicks on the tree items beneath it. Set
pointer-events: none so clicks pass through to the tree.
2026-06-12 22:47:40 +05:30
Dave Page 50f95f0fcb Preserve jsonb number representation in the JSON editor (#9854) (#10074)
The Query Tool's JSON cell editor pretty-printed jsonb values by parsing
and re-stringifying them with json-bignumber. While that preserves big
integers, it normalizes decimals through a JS float, so trailing
fractional zeros are dropped (10.00 -> 10, 3.140 -> 3.14). Because the
reformatted text is what gets written back, opening an unrelated jsonb
document and saving it silently rewrote numbers it never edited - which
can break applications that rely on the canonical jsonb text.

Switch the editor to lossless-json, which preserves the exact numeric
representation (big integers and trailing zeros alike), and pass it to
the underlying vanilla-jsoneditor as its parser so the in-editor format
action and tree/table modes are lossless too. The lossless helpers are
centralized in a small json_utils module with unit tests.

Closes #9854
2026-06-12 22:46:13 +05:30
Dave Page cc8ce7e153 Fix blank Query Tool screen caused by malformed runtime locale (#10066)
The Query History panel formats entry dates/times with
Date.prototype.toLocaleDateString()/toLocaleTimeString(). On runtimes
whose default locale (derived from the OS/environment) is malformed,
these throw "RangeError: Incorrect locale information provided".

Because getDateFormatted()/getTimeFormatted() are called during render
(via getGroups/getGroupHeader/getDatePrefix), the uncaught exception
unmounts the whole SQL editor React tree, leaving the user with a blank
white screen and losing any unsaved query work.

Guard both helpers and fall back to a moment-based format (moment uses
its own locale data and does not depend on the broken Intl default) so
the editor keeps working instead of crashing.

Closes #7596
2026-06-12 22:22:18 +05:30
Ashesh Vashi b458e69528 Merge remote-tracking branch 'origin/master' into cve-9.16
# Conflicts:
#	docs/en_US/release_notes_9_16.rst
2026-06-12 22:14:56 +05:30
Dave Page f6961bce49 Gracefully handle a missing crypt key in the Query Tool connection endpoints. (#10065)
After a backend/pod restart the in-memory crypt key is gone, so
manager.connection() raises CryptKeyMissing. The new-connection
endpoints (_check_server_connection_status and get_new_connection_*)
swallowed it in a broad "except Exception", logged a full ERROR
traceback, and returned a generic error the client cannot recognise.
The standard recovery (a 503 CRYPTKEY_MISSING response that the client
uses to transparently re-establish the key and retry) therefore never
fired, leaving a spurious "Crypt key is missing" message in the Query
Tool and noisy tracebacks in the log.

Re-raise CryptKeyMissing (along with ConnectionLost /
SSHTunnelConnectionLost) before the generic handler, matching the
pattern already used by the query execution path, so these endpoints
emit the standard CRYPTKEY_MISSING response and the client recovers
gracefully.

Closes #10027
2026-06-12 22:09:03 +05:30
Dave Page 922f06c7d2 Strip foreign-architecture code from the macOS bundle (#10063) (#10064)
The macOS app is built for a single architecture (matching the build
machine, via ${ARCH}), but relocatable-python pulls the python.org
universal2 installer, so the entire Python.framework ships both arm64
and x86_64 slices. PostgreSQL-sourced dylibs may be universal too. The
foreign slice is dead weight that bloats the bundle and DMG.

Add a _strip_architecture step, run after _complete_bundle and before
code-signing (lipo invalidates signatures, so the existing sign passes
re-sign the thinned binaries). It removes the universal2 stragglers
(python*-intel64 launcher, config-*-darwin/python.o) and lipo-thins
every fat Mach-O in the bundle to the build arch, preserving file modes
and warning on anything lacking the target slice. Already single-arch
inputs (Electron and its helpers) are skipped.
2026-06-12 15:57:09 +05:30
Dave Page 255550e981 Fix Query Tool password re-prompt loop for unsaved passwords (#9091) (#10073)
When a tool (Query Tool, View/Edit Data, etc.) opens a connection for a
server whose password was not saved, it relies on the password cached on
the server manager. If that cached password is unavailable, the tool
prompts for it. The entered password was POSTed to the connect_server
endpoint, which short-circuited with "Server connected" whenever the
server's primary connection was already established -- silently discarding
the entered password. The tool's own connection therefore still had no
password and re-prompted immediately, producing an infinite prompt loop
in which the re-entered password appeared to be rejected.

Cache the entered password on the server manager (encrypted) in that
short-circuit path so the tool's connection can reuse it. The password
overwrites any cached value, so a regenerated short-lived cloud auth
token (AWS RDS IAM / Azure Entra) takes effect immediately.
2026-06-12 15:51:01 +05:30
Dave Page 5a868bde90 Warn before opening a very large JSON value in the data grid (#9868) (#10075)
Opening a huge JSON/JSONB cell in the Query Tool's cell editor parses,
pretty-prints and renders the entire document on the main thread. For
pathologically large values (e.g. a jsonb object with 100k keys) this
blocks the UI thread and pgAdmin becomes completely unresponsive, with
no chance for the user to back out.

Guard the JSON editor: when the raw cell value exceeds a size threshold,
render nothing until the user confirms via a warning dialog. If they
cancel, the editor is closed without doing the expensive work. Small
values are unaffected and open immediately as before. The editor already
uses commitOnOutsideClick: false, so the confirm dialog does not dismiss
the editor.

The size-threshold logic is a small, separately tested helper.

Closes #9868
2026-06-12 15:41:35 +05:30
Dave Page 4fa95c58af Fix runtime menu refresh crash when menus not yet received (#9762) (#10070)
refreshMenus() rebuilt the application menu from the module-level
cachedMenus, which is only populated once the renderer sends its menu
definition via the 'setMenus' IPC. When a menu refresh was triggered
before that happened - e.g. an auto-update event, or the user closing
the window while the UI was still loading - cachedMenus was undefined
and bindMenuClicks() crashed with 'Cannot read properties of undefined
(reading map)', surfacing as an uncaught-exception dialog.

Guard refreshMenus() so it bails out when there are no cached menus to
rebuild.

Closes #9762
2026-06-12 15:38:19 +05:30
Dave Page ed9bf2f249 Fix syntax error editing SQL functions/procedures with a 'return' substring in the body (#10061)
The SQL-standard body detection used a regex that matched 'return'
anywhere in the body, so a plain SQL body containing a RETURNING clause
(or an identifier like 'returned_value') was wrongly treated as a
SQL-standard (BEGIN ATOMIC / RETURN) body. The generated CREATE OR
REPLACE statement then dropped the AS $BODY$ ... $BODY$ wrapper,
producing invalid SQL and a syntax error on save.

Anchor the RETURN form to the start of the body so only genuine
SQL-standard bodies are detected. Add unit tests for the detection.

Closes #10059
2026-06-12 15:25:34 +05:30
Dave Page 458dec3571 chore(deps): clear vulnerable transitive deps (tar, flatted) (#10049)
Resolves open Dependabot security advisories for transitive npm
dependencies that have no direct manifest entry (so Dependabot cannot
auto-open fix PRs for them).

tar (6 x HIGH): an old tar@6.2.1 was pulled in via
ttf2woff2@4.0.5 -> node-gyp@9.4.1 (and node-gyp's
make-fetch-happen@10 -> cacache@16 chain). ttf2woff2 6+ switched to an
ESM/default export that breaks @vusion/webfonts-generator's callable
usage, so rather than bump ttf2woff2 we override its node-gyp to
^11.2.0 via a scoped resolution. That modernises the whole sub-tree
(node-gyp 11, make-fetch-happen 14/15, cacache 19/20) onto tar@7.5.16
while keeping ttf2woff2 at 4.0.5 so webfont generation still works.

flatted (1 x HIGH): bumped 3.4.1 -> 3.4.2 in the Electron runtime
(GHSA-rf6f-7fwh-wjgh).
2026-06-12 14:50:43 +05:30
Dave Page 8dcc30a05d doc: Fix typo and clarify PGPASS_FILE documentation. #9866 (#10034)
Corrected "varible" -> "variable" and rewrote the broken sentence to
explain that PGPASS_FILE is the path to a pgpass file that is copied into
the container and used as the .pgpass file.
2026-06-12 14:49:41 +05:30
Dave Page 51e90a9827 Correct the macOS Replace shortcut in Query Tool toolbar docs (#9817) (#10033)
doc: Correct the macOS Replace shortcut in the Query Tool toolbar docs. #9817

The Edit toolbar table listed Replace as "Option + Cmd + F (MAC)" /
"Ctrl + Shift + F (Others)", but the default Replace shortcut is
Cmd/Ctrl + R, per register_editor_preferences.py.
2026-06-12 14:47:28 +05:30
Ashesh Vashi 4751795087 docs: clarify 9.16 release-notes entries for #10068 and #10078
Tighten the release-notes prose for the two security entries whose
original one-liner didn't carry the impact framing settled during
Dave Page's 2026-06-11 CVSS review.

#10068 (CVE-2026-12048): re-lead with the critical-severity chain.
Default pgAdmin Content-Security-Policy allows inline script and an
iframe srcdoc inherits the embedding origin, so attacker JavaScript
ran same-origin to the victim's authenticated pgAdmin session and
could read every saved server connection credential and issue
arbitrary SQL against every server the victim was connected to.
The previous wording described the sinks but not the impact.

#10078 (CVE-2026-12044): re-lead with the stored
pgstattuple/pgstatindex sink (low-privilege user names a table or
index foo'bar, superuser viewer triggers SQL under the superuser
role). That sub-defect is what earns the score; the description-
field self-injection is bundled because the fix is the same. The
previous wording led with the self-injection only.

Both rewrites match the descriptions used in the CVE JSON records
submitted to the PostgreSQL CNA.
2026-06-12 09:49:44 +05:30
Ashesh Vashi cc4deed6eb docs: add CVE numbers and security fix entries to 9.16 release notes
Add CVE-2026-12044 through CVE-2026-12050 to the Bug fixes section and
set the release date to 2026-06-18.
2026-06-12 09:15:44 +05:30
Ashesh Vashi 04977b593e Merge remote-tracking branch 'origin/master' into cve-9.16 2026-06-12 08:58:28 +05:30
Dave Page 0ff9ecfe67 test(llm): pin sandbox-weakening commands and parser edge cases
Extend the read-only query validator's regression suite with 17 more
scenarios lifted from a v2 of the original #10022 patch that was
never committed (recovered from an uncommitted draft).

Five new accept scenarios cover parser corner cases:
  - leading whitespace
  - trailing semicolon followed by trailing whitespace
  - leading line comment
  - semicolon inside a string literal (must not split the statement)
  - (SELECT 1) UNION (SELECT 2), confirming the leading-keyword
    traversal walks past parentheses

Twelve new reject scenarios pin commands that directly attack the
BEGIN TRANSACTION READ ONLY wrapper rather than merely attempting a
generic write:
  - bare COMMIT / END / ROLLBACK / ABORT / BEGIN as single statements
    (previously tested only as the leading verb of a multi-statement
    payload)
  - START TRANSACTION (synonym for BEGIN)
  - SAVEPOINT (savepoint manipulation)
  - SET LOCAL transaction_read_only = off
  - SET SESSION default_transaction_read_only = off
  - DISCARD ALL (resets session state -- search_path, prepared
    statements, etc.)
  - multi-statement payloads where the leading statement is an
    allowed verb (SELECT or WITH) followed by ROLLBACK and a write
    -- closer to a real attack shape than the trailing-COMMIT PoC

If a future refactor of _ALLOWED_LEADING_KEYWORDS or the statement-
counting logic lets any of these slip through, the new scenarios
fail loudly. Module is now 77 passed / 0 failed / 0 skipped.
2026-06-11 10:11:28 +05:30
Yogesh Mahajan 7aa3f85caf fix(#9701): Ensure to use psycopg3 is used while connecting to postgres DB when PGADMIN_CONFIG_CONFIG_DATABASE_URI is specified. (#9914) 2026-06-11 09:55:09 +05:30
Ashesh Vashi 2ae0d36109 fix(sqli): close remaining apostrophe-in-identifier sinks and tighten the lint
Layered follow-up to the COMMENT description SQLi fix. Closes three
related gaps the original patch did not cover.

1. Stats templates — apostrophe-in-identifier SQLi (10 sites).

The pgstattuple/pgstatindex call sites in the stats templates rendered
the target relation as an embedded single-quoted literal, e.g.

    pgstattuple('{{schema_name}}.{{table_name}}')
    pgstatindex('{{conn|qtIdent(schema)}}.{{conn|qtIdent(name)}}')

A user with CREATE privilege on a schema could plant a table or index
named `foo'bar` (PostgreSQL permits any character except NUL inside a
quoted identifier). Any viewer who then opened that object's stats
panel would render an unbalanced SQL literal. qtIdent does NOT escape
apostrophes — it escapes the embedded double quotes that delimit a
quoted identifier — so the outer single-quoted literal still broke
out. Same bug class as the COMMENT description vector, just gated on
DDL privilege instead of dialog access.

Fix replaces all 10 sites with the canonical regclass form:

    pgstattuple({{ tid }}::oid::regclass)
    pgstatindex({{ exid|cid|idx }}::oid::regclass)

The OID is already passed by each handler (tables/utils.py,
exclusion_constraint/__init__.py, index_constraint/__init__.py,
indexes/__init__.py, views/__init__.py). The cast eliminates the
embedded string literal entirely — the bug class can no longer recur
through these sites.

Files:
  tables/sql/{default,16_plus}/stats.sql
  views/templates/mviews/{pg,ppas}/default/sql/stats.sql
  tables/templates/exclusion_constraint/sql/{default,16_plus}/stats.sql
  tables/templates/index_constraint/sql/{default,16_plus}/stats.sql
  tables/templates/indexes/sql/{default,16_plus}/stats.sql

2. Lint regex — embedded-Jinja blind spot.

The original lint regex `'\{\{[^}]+\}\}'` only matched literals where
the Jinja interpolation filled the entire single-quoted body. It
missed the embedded form `'foo{{ x }}bar'` — exactly the shape used
by the stats templates above and by `'%{{ search_text }}%'` in
search_objects.

The broadened regex `'[^'\n{}]*(?:\{\{[^}]+\}\}[^'\n{}]*)+'` catches
both forms. The `[^'\n{}]` body constraint is what stops the regex
from spanning a Jinja control block `{%...%}` — without it the regex
would walk across long `{% if %}` chains and produce huge false-
positive matches that span unrelated literals.

Allowlist extended with one-line justifications for every additional
occurrence the broadened regex now surfaces (search_objects manual
escape, CATALOGS.LABELS_SCHEMACOL macro, gettext-translated catalog
labels, opcintype oidvector pairs, the pg_get_partkeydef CASE
expression where the interpolation is between two separate literals
rather than inside one).

3. qtLiteral — close the inner-except silent-failure hole.

The fail-fast for a missing `conn` was the right shape but the inner
`except Exception: print(value)` still silently returned the raw
input when `psycopg.sql.Literal(value).as_string(conn)` raised
(unadapted custom type, encoding error, etc.). Same SQL-injection
sink the outer fail-fast just plugged, gated on a different failure
mode. Removed the except so the exception propagates — callers can
now react explicitly instead of seeing an unescaped value flow into
their SQL.

Tests:
  test_stats_template_regclass_cast.py — 10 scenarios that render
    each stats template with an apostrophe-bearing identifier payload
    and assert (positive) the pgstat* call uses ::oid::regclass and
    (negative) no `pgstat*('...'` form recurs, and the apostrophe
    payload never appears inside a literal in the rendered SQL.
  test_qtliteral_no_silent_unescape.py — asserts qtLiteral raises on
    an unadaptable value rather than silently returning it.

42 tests / 0 failures across the touched packages.
2026-06-10 21:37:47 +05:30
Dave Page 658bb585d1 fix(sqli): HTML-escape description fields and harden qtLiteral
Fixes a SQL injection vulnerability where authenticated users could
break out of SQL string literals in COMMENT ON ... IS '<description>'
clauses by submitting an apostrophe-laden description through pgAdmin
dialogs. The original report covered domains; the patch expands the
fix to every site of the same pattern.

Three layers of defense:

1. Site fixes (16 places) — Replace '{{ x.description }}' with
   {{ x.description|qtLiteral(conn) }} across templates for domains,
   domain constraints, foreign tables, languages, event triggers,
   and the views OID-lookup query. Plumbs conn=self.conn through
   every render_template call that needed it. Also fixes a `{ % elif`
   Jinja typo in foreign-table schema diff that was preventing the
   elif branch from being reachable.

2. Driver hardening — qtLiteral (in utils/driver/psycopg3/__init__.py)
   used to silently return the raw unescaped value when conn was
   falsy. Now raises ValueError with a message pointing at the two
   fixes (render_template(..., conn=) or pass conn as the second
   argument). Surfaces this whole bug class loudly going forward,
   and immediately uncovered 8 latent plumbing bugs in
   schemas/__init__.py, schemas/functions/__init__.py,
   schemas/tables/utils.py, foreign_servers/__init__.py, and 7 sites
   in roles/__init__.py — all now fixed.

3. Regression tests (3 new files):
   - test_comment_description_sql_escaping.py — renders each
     previously-vulnerable template with an apostrophe-injection
     payload and asserts the escaped fragment is present (15
     scenarios).
   - test_sql_string_literal_lint.py — walks every *.sql template,
     flags every '{{ ... }}' single-quote-wrapped Jinja
     interpolation, and compares against a curated allowlist (75
     entries, each with a justification — OIDs, fixed enums,
     server-derived identifiers, SQL-comment headers, etc.). New
     occurrences fail the test until either qtLiteral is used or
     an allowlist entry is added.
   - test_qtliteral_requires_conn.py — unit test asserting the new
     fail-fast behavior.

Reported by Jasser Chebbi (j3seer).
2026-06-10 21:37:47 +05:30
Ashesh Vashi f81433ae2f Fix RCE via unauthenticated session deserialization in SQL Editor close/update routes.
The 'close' (DELETE /sqleditor/close/<trans_id>) and
'update_sqleditor_connection' (POST /sqleditor/initialize/sqleditor/
update_connection/...) endpoints were the only state-mutating SQL Editor
routes missing @pga_login_required. Both reach pickle.loads on
session['gridData'][trans_id]['command_obj'] via
close_sqleditor_session() and check_transaction_status() respectively.

Combined with a forged session file (precondition: SECRET_KEY leak +
write access to sessions/), this gave an unauthenticated attacker a
pickle deserialization sink and arbitrary code execution in the pgAdmin
process. Adding the login decorator forces is_authenticated/MFA checks
before the unsafe deserialization path is reached, matching the
convention used by every other endpoint in the module.

Includes a server-mode regression test that harvests a CSRF token from
GET /login (mirroring the attacker's path) and asserts both endpoints
reject the unauthenticated request before reaching the route body.
Self-skips in DESKTOP mode because pgAdmin's before_request hook
re-authenticates DESKTOP_USER on every request there, so no auth
decorator can be exercised in an unauthenticated state. Wired into the
existing server-mode CI workflow alongside the data-isolation tests.

Reported by Fernando Bortotti <fernando.bortotti@bsd.com.br>.
2026-06-10 20:01:44 +05:30
Ashesh Vashi 60d149864b fix(cloud): HTML-escape SDK error text across cloud module endpoints
Promote the post-connection-SQL sanitiser to a generic helper and apply
it to every cloud-module endpoint that propagates AWS / Azure / Google
SDK exception text into a JSON response field. Closes the RDS
HTML-injection vector reported against /rds/verify_credentials/ and
sweeps the same pattern across the verify-credentials, deploy, regions,
and update-server paths so no remaining cloud endpoint embeds raw
SDK / OS exception text into the response.

Sanitiser
  - Move web/pgadmin/utils/driver/psycopg3/text_sanitize.py to
    web/pgadmin/utils/text_sanitize.py.
  - Rename sanitize_driver_message -> sanitize_external_text; the
    function now describes its real role (HTML-escape text from any
    external/untrusted source — driver, cloud SDK, OS process).
  - Move tests to web/pgadmin/utils/tests/test_text_sanitize.py.
  - Update the single existing importer in psycopg3/connection.py.

Backend wrap sites
  - misc/cloud/rds/__init__.py — verify_credentials info= (the
    reported case: AWS STS IncompleteSignature echoes access_key
    verbatim into the exception string) and get_regions errormsg=
    (boto3 Session().get_available_regions exception).
  - misc/cloud/azure/__init__.py — verify_credentials and
    check_cluster_name_availability errormsg= (3 sites).
  - misc/cloud/google/__init__.py — verify_credentials
    (PermissionError + generic Exception path-resolution branches and
    the get_auth_url error), verification_ack, projects, regions,
    instance_types, database_versions — 8 sites covering every path
    that propagates SDK / file-resolution exception text.
  - misc/cloud/__init__.py — central /deploy endpoint (errormsg=resp
    where resp is str(e) bubbled up from deploy_on_rds /
    deploy_on_azure / deploy_on_google) and update_cloud_server
    (errormsg=server where server is the str(e) from db.session.commit
    failure).

Frontend
  - components/FormComponents.jsx — declare plainText in
    FormFooterMessage.propTypes (it already passes through via
    spread).
  - misc/cloud/static/js/CloudWizard.jsx — add plainText to the three
    FormFooterMessage sites. INFO messages there are gettext literals,
    so plain-text rendering is uniformly safe; the ERROR path now
    follows the SafeMessage contract for backend-derived strings.

Tests
  - rds/tests/test_rds_verify_credentials_xss.py — wiring test that
    patches RDS.validate_credentials to return an AWS-shaped error
    embedding an <iframe> payload, asserts the response info field is
    entity-encoded and contains no raw markup. Includes a happy-path
    guard against the sanitiser altering 'verified'.
  - azure/tests/test_azure_verify_credentials_xss.py — same pattern,
    Azure.__init__ touches current_user so the Azure class is patched
    to a stand-in that yields the HTML-bearing validation error.
  - google/tests/test_google_verify_credentials_xss.py — covers the
    PermissionError and generic-Exception branches of the
    client_secret_file resolution path (most directly user-influenced).
  - test_post_connection_sql_xss.py — docstring + comment updates for
    the renamed function.

Frontend rendering is already DOMPurify-guarded at the NotifierMessage
sink, so the reported PoC payload would not execute even without
these backend changes; the fixes here are the defence-in-depth layer
the report's section-5 "Suggested directions" calls out, ensuring API
consumers other than the browser (audit logs, third-party clients)
never see raw markup.

876 / 876 JS tests + ESLint clean. Cloud / utils / server-groups
Python test packages pass (24 + 60 + 102, with pre-existing skips
unchanged).
2026-06-10 20:01:44 +05:30
Dave Page 9e370d3cb6 fix(xss): comprehensive XSS hardening across notification and Explain flows
Mitigates a stored XSS vector where a malicious PostgreSQL server's
ErrorResponse — or any backend-derived string — could inject HTML into
pgAdmin's DOM via the notifier, Explain visualiser, or form-input error
paths. Combines three complementary layers of defence.

Frontend — DOMPurify at every HTMLReactParse sink:
  - NotifierMessage (toasts) and FormFooterMessage
  - FormInput help / error messages, FormNote
  - ModalProvider AlertContent + confirmDelete
  - ToolErrorView (tool open failure)
  - Explain/Analysis NodeText (plan extra-info renderer)
  - SQL editor confirm dialogs (transaction, promotion, close-running)
  - Dialogs/ConfirmSaveContent
  - PreferencesHelper modal alert
  - SelectThemes helper text
  No HTMLReactParse call site renders text unsanitised after this.

Frontend — plain-text contract for backend-derived strings:
  - New components/SafeMessage.jsx exports SafeMessage (plain text,
    preserves newlines via pre-wrap) and SafeHtmlMessage (sanitised
    HTML via DOMPurify + html-react-parser).
  - NotifierMessage and ModalProvider.AlertContent accept a `plainText`
    prop. When true the body renders via SafeMessage.
  - Notifier gains errorText / alertText / warningText / infoText /
    successText methods that plumb {plainText: true} through. These
    are the correct choice whenever the message may originate from a
    PostgreSQL server, driver, OS process, remote API, or any other
    untrusted channel.
  - pgRespErrorNotify and pgNotifier use alertText / errorText
    internally. The redundant _.escape and \\n -> <br/> substitution
    in pgNotifier are gone — the SafeMessage container's pre-wrap
    preserves newlines without further transformation.
  - FormInput.errorMessage and ToolErrorView render as plain text
    unconditionally; their inputs are always validation / driver
    errors.
  - ~50 callers across browser, tools, dashboard, debugger, misc, llm,
    preferences, schema diff, and the SQL editor are migrated to the
    *Text variants where they pass a backend-derived field
    (err.response.data.errormsg, parseApiError(error), res.errormsg,
    res.info, res.data.result, err.message, interpolated PG object
    names).
  - Browser auto-update notification rewritten to plain text — the URL
    is shown inline instead of being interpolated into an <a href>
    attribute. The gettext template-literal anti-pattern on that path
    is fixed too (URL is a separate %s argument).

Cleanup:
  - helpers/Notifier.jsx — remove the dead AlertContent function and
    its seven imports (Box, CloseIcon, DefaultButton, PrimaryButton,
    HTMLReactParser, CheckRoundedIcon). The function was defined but
    never instantiated; the real top-level alert path goes through
    ModalProvider.AlertContent. Eliminates a false-positive sink that
    recent audits cite — the cited line numbers reference a function
    that never runs.

Explain — escape Recheck Cond / Exact Heap Blocks:
  - nodeExplainTableData concatenates plan-node fields into HTML
    strings parsed by html-react-parser. Every sibling field (Filter,
    Index Cond, Hash Cond, Join Filter, Rows Removed by Filter, Hash
    Buckets/Batches/Peak Memory) wraps the value in _.escape();
    Recheck Cond and Exact Heap Blocks were the two outliers
    concatenating raw. An attacker who introduces a table with a
    crafted column name and triggers a Bitmap Heap Scan over it could
    inject HTML into Explain output that a victim later views in the
    visualiser. With the rest of the XSS fix this would otherwise be
    neutralised by DOMPurify at the sink — escaping at construction
    matches the sibling fields and provides defence in depth.

Backend — HTML-escape PG-returned text in post-connection-SQL:
  - New web/pgadmin/utils/driver/psycopg3/text_sanitize.py exports
    sanitize_driver_message. Strips C0 control characters
    (0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F, 0x7F) — preserving TAB, LF,
    CR so multi-line error messages stay readable — then HTML-escapes
    via html.escape(..., quote=True).
  - execute_post_connection_sql logs the raw PG-returned text so the
    server log stays human-readable, and applies the escape only to
    the value crossing into the JSON response body, where downstream
    consumers may render it as HTML:
      <iframe srcdoc="..."> -> &lt;iframe srcdoc=&quot;...&quot;&gt;
    Real Postgres errors (which never contain literal markup) round-
    trip unchanged for human readability. Protects third-party JSON
    consumers (audit logs, API clients, support tickets) that do not
    have the frontend's DOMPurify.

Tests
  - components/SafeMessage.spec.js — 25 payloads (iframe srcdoc, SVG
    onload, MathML href javascript:, embed/object data:, marquee
    onstart, template/noscript, CSS expression, etc.).
  - Notifier.spec.js — 9 cases (errorText / alertText plumbing,
    pgRespErrorNotify 410 vs non-410, pgNotifier raw errormsg pass-
    through, "Unknown error" fallback).
  - ToolErrorView.spec.js — 3 cases.
  - ModalProvider.spec.js — 4 cases.
  - FormComponents.spec.js extended with NotifierMessage plainText
    mode + FormInput errorMessage plain-text rendering.
  - Explain regression test renders Analysis with HTML-laden extraInfo
    and asserts no <iframe> / <script> reaches the DOM.
  - Python: test_sanitize_driver_message.py (21 sanitiser + 5
    execute_post_connection_sql wiring scenarios with a real Flask +
    flask-babel app context and a stand-in connection).
  - test_post_connection_sql_xss.py — end-to-end integration: register
    a server, set post_connection_sql to a query referencing a relation
    whose name contains an iframe payload, POST to the connect endpoint,
    and assert the response JSON errormsg is entity-encoded and never
    raw markup.
  - fake_pgadmin.js notifier mocks and enable_disable_triggers_spec.js
    spies updated for the new API.

875 / 875 JS tests pass; ESLint clean.
2026-06-10 20:01:44 +05:30
Khushboo Vashi 04fa05c1e6 Fix missing ALTER ... SET DEFAULT for inherited columns in table SQL. (#9774)
When a table inherits a column from a parent, the generated table
SQL/EDIT script omitted the column's default. Emit a post-create
ALTER TABLE ... ALTER COLUMN ... SET DEFAULT for inherited columns
that carry a default, and show the default in the inherited-column
comment, across all version buckets.

Generated columns (colconstype 'g') are excluded from both the new
ALTER statement and the comment default, consistent with the inline
column-definition logic, since SET DEFAULT is invalid for a generated
column.
2026-06-09 15:44:22 +01:00
ZoroXL 77dc9d77ec Fix infinite loading after an idle connection is dropped (#6308)
When a database connection was silently dropped while pgAdmin sat idle
(common on Linux Desktop behind a firewall/NAT), the Object Explorer and
Query Tool would hang on an infinite spinner instead of offering to
reconnect, because connected() only checks local driver state and misses
stale/half-open TCP sockets.

- ping() now performs a real network check (SELECT 1) guarded by the
  connection's transaction_status, so an in-progress query or open
  transaction is never disrupted; on failure it tears down the dead
  connection and returns False.
- The Object Explorer children() endpoint uses ping() to detect a dead
  connection up front and returns 503 CONNECTION_LOST; the tree shows a
  reconnect dialog (deduplicated per server) and marks the node
  disconnected.
- TCP keepalive defaults are applied to all connections so the OS
  surfaces dead sockets in seconds rather than the full retransmission
  timeout.
- The Query Tool re-checks connection status when its tab becomes
  visible again, and its layout/visibility listeners are now cleaned up
  on unmount.

Refs #9700, #8279. Adds a 9.16 release note.
2026-06-09 15:26:28 +01:00
Manolis Stamatogiannakis 8ad45be9c8 Use ServerManager's passfile in connect() credential gate. (#9810)
The passfile kwarg passed to Connection.connect() was only ever used as
a gate for the passexec fallback; it was never forwarded to
create_connection_string(), which builds the DSN's passfile from the
ServerManager's connection parameters. This made the gate inconsistent
with the passfile actually used for the connection.

Use the ServerManager passfile for the credential gate so the check
matches what is used to connect. The manager passfile now takes
precedence over both passexec and any passfile kwarg; warnings are
emitted when either is ignored in its favor.
2026-06-09 15:23:50 +01:00
Dave Page 2918e16734 docs: move misfiled release-note entries from 9.9 to 9.16
The entries for #9875 (EXPLAIN/blank-line query extraction) and #9988
(OAuth2 metadata URL guidance) were added to release_notes_9_9.rst,
which is an already-released version. Move them to the in-progress
9.16 notes where they belong.
2026-06-09 15:22:33 +01:00
Daniel Zabel 06300e76a0 Make init container security context configurable in the Helm chart (#9646)
The two init containers in the Helm deployment template had hardcoded
securityContext blocks, unlike the main container which already renders
its context from .Values.containerSecurityContext via the
renderSecurityContext helper. Switch the init containers to the same
pattern so operators can customise (or disable) their security context.

Default behaviour is unchanged: containerSecurityContext defaults to
enabled with values identical to the previous hardcoded block, and the
helper continues to gate appArmorProfile on
global.compatibility.appArmor.enabled. Verified with helm template that
the rendered init-container securityContext is unchanged for the default
values, honours the appArmor toggle, and is omitted entirely when
containerSecurityContext.enabled=false.

Adds a 9.16 release note.
2026-06-09 15:17:01 +01:00
Ashesh Vashi 397be4e70e Fixed an issue where EXPLAIN and EXPLAIN ANALYZE failed when blank lines separate clauses in the SQL query. (#9875)
getQueryAt now uses the syntax tree as the primary check for whether a
blank-line boundary cut through a SQL statement. It is split into two
helpers: _findQueryBoundaries (the original scan, parameterized by
stopAtBlankLine) and _needsExpansion, which detects when a Statement
node straddles the extracted range and re-scans ignoring blank lines.

A STATEMENT_STARTERS keyword list guards the case where the parser
merges semicolon-less queries into one Statement, and WRAPPER_STARTERS
(EXPLAIN, ANALYZE, WITH) force expansion when the Statement extends past
the range. Also fixes a Lezer boundary bug (tree.iterate is inclusive at
boundaries) with a node.to > startPos check. Adds 21 tests covering
EXPLAIN, negative no-merge cases, comments, and boundary edge cases.
2026-06-09 15:10:07 +01:00
Adam Tao 0223e8a026 fix(openai): tolerate empty/null fields in streaming tool-call deltas (#9828)
Some OpenAI-compatible providers emit empty or null name/arguments/id
fields in streaming continuation deltas to keep the response schema
stable. pgAdmin's accumulator overwrote the real tool name (captured in
the first delta) with the later null, producing a tool call named "null"
that could not be dispatched.

Skip falsy name/arguments/id when accumulating (matching the OpenAI
Python SDK, which ignores nulls the same way) so the values captured in
the first delta survive. Also guard against a null `function` object in
a delta, which previously raised TypeError. Without the id guard a null
id in a continuation delta clobbered the real id, which the final build
then replaced with a random uuid rather than the provider's id.

Adds a unit test covering the null-continuation, multi-chunk-arguments,
and null-function cases, and a 9.16 release note.
2026-06-09 15:08:19 +01:00
Ashesh Vashi 89e84eb913 OAuth2: actionable error when openid scope lacks OAUTH2_SERVER_METADATA_URL (#10007)
When OAUTH2_SCOPE contains 'openid' but OAUTH2_SERVER_METADATA_URL is
not set, Authlib fails deep inside id_token verification with a cryptic
`Missing "jwks_uri" in metadata` error that names neither the config
knob nor the fix.

Add a pre-flight check at the entry of _authorize_access_token: if the
scope includes 'openid' and no (non-whitespace) metadata URL is set,
raise a RuntimeError with actionable guidance before any network
round-trip. server_metadata_url is the only way pgAdmin feeds JWKS to
Authlib, so this carries no regression risk for correctly-configured
providers. Clarify the OAUTH2_SERVER_METADATA_URL comment in config.py
and add regression coverage.
2026-06-09 14:53:28 +01:00
Ashesh Vashi 879ae40cbb CLI: skip delete_adhoc_servers() in CLI mode (#10008) 2026-06-09 14:47:48 +01:00
Ashesh Vashi c88c8f612e docker: read PGADMIN_CONFIG_CONFIG_DATABASE_URI safely via os.environ (#9984)
The container entrypoint substituted ${PGADMIN_CONFIG_CONFIG_DATABASE_URI}
into a double-quoted Python string for `python3 -c`. Combined with the
config_distro.py convention (where the env var's value must itself be a
Python literal, i.e. users set it to 'postgresql+psycopg://...'), the
entrypoint re-wrapped the already-quoted value, producing a string with
literal quotes inside that SQLAlchemy could not parse -- and the Python
crash made the first-launch check capture an empty string, silently
skipping PGADMIN_DEFAULT_EMAIL / PGADMIN_DEFAULT_PASSWORD setup.

Read the env var inside Python via os.environ so the shell no longer
participates in Python-literal quoting (also removing a shell-injection
surface), and use ast.literal_eval to unwrap the legacy quoted form while
letting raw values pass through. external_config_db_exists now stays
"False" on any Python failure so first-launch setup still runs.

Adds a 9.16 release note.
2026-06-09 14:44:26 +01:00
Ashesh Vashi 24f0597dc8 Server UI: clarify SSH tunnel identity-file password prompt label (#10010) 2026-06-09 14:35:42 +01:00
Ashesh Vashi 303f6a2dd8 Use resolve_fks=False in migration reflect() so stale FKs don't crash startup (#9976)
An old configuration database (e.g. a pre-9.0 pgadmin4.db lingering in
%appdata%) can carry a stale foreign key such as server.user_id ->
user_old, a table removed long ago. SQLAlchemy's MetaData.reflect()
defaults to resolve_fks=True, which auto-follows the reflected table's
foreign keys and reflects their targets too; the orphan target trips
NoSuchTableError and aborts the startup migration, surfacing in the GUI
as the misleading "Server could not be contacted".

None of the migrations use the FK-target tables -- each operates only on
the explicitly requested table(s). Pass resolve_fks=False at all 14
meta.reflect() call sites across the 12 migration files so reflection no
longer cascades into broken FK targets. Behaviour is unchanged for
healthy databases.

Adds a 9.16 release note.
2026-06-09 14:32:19 +01:00
Dave PageandClaude Opus 4.8 8afe14ec36 Remove design proposal document committed in error
This internal design proposal was inadvertently included with the
session and file-manager hardening changes and should not have been
added to the repository.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:31:10 +01:00
Ashesh Vashi 25415b888a docs(container): warn about K8s init-container tag mismatch and RollingUpdate hazard on shared data volumes (#10014) 2026-06-09 14:28:16 +01:00
Pavan Manish c720a33b44 Add toast_tuple_target to the Materialized View dialog (#9626)
Exposes the PostgreSQL 11+ TOAST_TUPLE_TARGET storage parameter in the
Materialized View properties dialog, mirroring the existing FILLFACTOR
support: an integer field (128-8160) in the Definition group, extracted
from reloptions in properties.sql, emitted in the CREATE ... WITH clause,
and SET/RESET on edit. The recreate-on-definition-change path preserves
the value.

The dialog field and SQL are applied to the version buckets that
supported servers actually resolve to. The original change only touched
the legacy default/ bucket (PG < 12), so it had no effect on any
supported server (13-18, which resolve to 13_plus/15_plus); the
create/properties/update templates are now updated in the 12_plus,
13_plus and 15_plus buckets too. Template output is byte-identical to
the previous SQL when toast_tuple_target is unset.

Adds a 9.16 release note.
2026-06-09 14:16:50 +01:00
Ashesh Vashi f9b9bc9d40 fix(sqleditor): skip read-only columns when saving inserted rows (#10015) 2026-06-09 14:12:11 +01:00