Commit Graph
8300 Commits
Author SHA1 Message Date
Ashesh Vashi 6e029ab6e5 docs: correct reporter credit for #10193/#10194 to full name
Hung Tran Quoc (GitHub handle @rampage0010) — release notes only
listed the given name.
2026-07-28 12:40:25 +05:30
Ashesh Vashi ee7109d400 Updated version for release v9.17 2026-07-28 12:08:09 +05:30
Ashesh Vashi 78f7a5f6ab test: restore app.PGADMIN_EXTERNAL_AUTH_SOURCE in auth-mocking tearDown
Each of the Kerberos/LDAP/OAuth2/webserver login-mocking test classes
sets self.app.PGADMIN_EXTERNAL_AUTH_SOURCE (the Flask app instance
attribute before_request() actually reads) in setUp, but tearDownClass
only restored app_config.PGADMIN_EXTERNAL_AUTH_SOURCE -- a different
object (the config module). Whichever of these classes ran last
(test_webserver_with_mocking, alphabetically) left the live app
attribute stuck at WEBSERVER for the rest of the suite, making
before_request() route every subsequent unauthenticated /login request
through authenticate.login()'s webserver auto-auth path instead of
rendering the login form -- surfacing as an unrelated CSRF-harvest
failure in test_close_requires_auth much later in the run.

Full server-mode regression: 2507 tests, 0 failures/errors (was 2).
2026-07-28 11:07:37 +05:30
Ashesh Vashi 689d200e9f test: fix server-mode test-harness bugs found running the full suite
get_test_user() created secondary test clients via app.test_client()
but never called setApp() on them, so any test using a second user
crashed with 'NoneType has no attribute config' the first time it
needed to fetch a CSRF token (44 errors in a full server-mode run).

TestSqlEditorCloseRequiresAuth.setUp() logs out the shared
class-level tester before asserting a CSRF token was harvested; when
that assertion failed, unittest skipped tearDown() entirely (by
design, tearDown only runs if setUp succeeds), leaving the tester
logged out for the rest of the suite and cascading into 188 unrelated
failures. Switched to addCleanup(), which always runs.

Full server-mode regression: 2507 tests, was 232 failures/errors,
now 2 (both isolated to this same test's own CSRF-harvest timing in
full-suite order, unrelated to these two fixes).
2026-07-28 10:12:18 +05:30
Ashesh Vashi 339b4f9cd3 Merge branch 'cve-9.17-rebase' into master 2026-07-28 09:12:21 +05:30
Domenico Sgarbossa 5fc319551d Updated message catalogs for v9.17 (#10212) 2026-07-28 09:00:14 +05:30
Ashesh Vashi 9ef9a7136f docs: add 9.17 release notes for CVE-2026-17346 through 17351, 17566
Adds bug-fix bullets for issues #10190-10194, #10200 (previously
assigned CVE-2026-17346..17351) and #10213 (CVE-2026-17566, the
import/export \copy backslash-escape RCE reported by Arpit Jain).
2026-07-28 08:56:41 +05:30
Ashesh Vashi 1496fabe28 fix(security): reject ambiguous backslash-escape in import/export query guard
_is_query_parens_balanced() always treated \' inside a single-quoted
string as an escaped quote, matching psql only when
standard_conforming_strings=off. Under the actual default (scs=on,
every supported PostgreSQL version), psql treats \ as a literal
character, so 'a\' closes the string right there. A crafted query
export payload like SELECT 'a\') TO PROGRAM 'cmd' x' passed the
balance check while the real ) it hid closed the wrapping \copy (...)
context in psql, exposing a live TO PROGRAM clause for RCE.

Since the correct interpretation depends on a server setting we
can't reliably know, reject any backslash inside a single-quoted
string outright instead of guessing.

Reported by Arpit Jain (arpitjain099).
2026-07-27 19:04:29 +05:30
Kundan ef76102bcd fix(llm): close lexer-differential bypass in AI Assistant read-only guard
sqlparse's string-literal lexing can disagree with PostgreSQL's: under
standard_conforming_strings = on (the default), a backslash before a
quote is an ordinary character to PostgreSQL but sqlparse treats it as
escaping the quote, so a payload like
SELECT '\';COMMIT;CREATE TABLE pwn(x int);SELECT 1 --'
passes _validate_readonly_query as a single SELECT while PostgreSQL
executes it as four statements -- the smuggled COMMIT ends the wrapping
BEGIN TRANSACTION READ ONLY and the trailing ROLLBACK is a no-op,
reintroducing the write/RCE bypass the bf47924444 fix was meant to
close (reported by Kai Aizen / SnailSploit).

Run the LLM-supplied query with prepare=True, forcing psycopg3's
extended query protocol. PostgreSQL's own Parse step -- not a
client-side approximation of it -- rejects any text containing more
than one statement, independent of how it's lexed. Threaded through
execute_2darray as an opt-in parameter (default None) so no other
caller's behavior changes. Also set SESSION CHARACTERISTICS AS
TRANSACTION READ ONLY as defense-in-depth against a smuggled COMMIT.

prepare=True alone is not sufficient: psycopg3's PrepareManager.get()
returns Prepare.NO -- checked before it even inspects the prepare
argument -- whenever the connection's prepare_threshold is None, which
is pgAdmin's per-server default ("Prepare threshold" is blank unless an
administrator sets it). On a default-configured server the extended
protocol never actually engaged, so the bypass stayed live. Force
prepare_threshold=0 on the LLM's single-use connection in
_connect_readonly() so the extended protocol -- and PostgreSQL's
single-statement Parse-step guarantee -- is unconditional on this
connection, without touching the server-wide setting or any other
session/caller.

Adds regression coverage at three levels: unit tests pinning that
prepare=True is always passed and that prepare_threshold=0 is forced
on the connection; and an end-to-end test driving the real
/sqleditor/nlq/chat/<trans_id>/stream route (LLM client mocked, real
tool-dispatch path) with the exact smuggled-COMMIT payload, confirming
the guarantee holds from the HTTP entry point down to the driver call.
2026-07-27 16:51:46 +05:30
Ashesh Vashi 2b4307f321 feat: add system-wide default for Geometry Viewer custom tile provider (#10198)
Follow-up to #10142. Geometry Viewer's custom tile provider was only
configurable per-user (Preferences), with no way for an administrator
to set an organization-wide default (e.g. an internal tile server)
that applies out of the box for every user.

config.DEFAULT_GEOMETRY_VIEWER_PROVIDER is a plain dict; the five
custom_tile_* preference defaults (url, name, crs, attribution,
max_zoom) are sourced from it instead of hardcoded literals.
Preference.get() only reads a per-user DB row if the user has
explicitly saved one, otherwise it falls back to this default --
override semantics, one active provider, no naming-conflict surface.

config_local.py/config_distro.py/PGADMIN_CONFIG_* replace this config
variable wholesale, not merged key-by-key, so a naive
DEFAULT_GEOMETRY_VIEWER_PROVIDER['name'] lookup would KeyError-crash
preference registration (and app startup) on any partial admin
override, or AttributeError on a wrong-typed value.
resolve_geometry_viewer_provider_defaults() guards against this:
falls back per-field on missing/wrong-type values, validates crs
against the 3 supported choices, validates max_zoom is a non-bool int
in [0, 25], degrading to the original hardcoded defaults instead of
crashing.

No new attacker-reachable surface: DEFAULT_GEOMETRY_VIEWER_PROVIDER is
filesystem/deployment-level admin config, same trust boundary as
DEFAULT_BINARY_PATHS/OAUTH2_CONFIG. Existing DOMPurify sanitization on
name/attribution and the http(s):// + {x}/{y}/{z} URL validation in
GeometryViewerUtils.js (from #10142) apply uniformly to config-sourced
and per-user values alike.

Adds test_geometry_viewer_provider_defaults.py (9 scenarios: fully
valid, partial override, non-dict, invalid CRS, out-of-range/non-int/
bool max_zoom, non-string fields, empty dict).
2026-07-27 16:07:17 +05:30
Kundan Sable 362c202b25 fix: close remaining front-door-only tool-RBAC gaps (tools_ai, import_export_servers, search_objects, change_password)
Extends the tool-RBAC fix (permission enforced only on a blueprint's
"front door" route, with the rest relying on @pga_login_required alone)
to four permission types the original fix didn't cover:

* tools_ai ("AI Reports"): none of the 14 Security/Performance/Design
  report routes in the llm blueprint checked this permission at all --
  only the frontend menu was hidden behind AllPermissionTypes.TOOLS_AI
  (llm/static/js/ai_tools.js). A user an admin had denied AI access to
  could call the report/report-stream routes directly and have real
  server/database/schema content sent to whichever external LLM
  provider is configured, bypassing both the access control and the
  data-exposure/cost boundary the permission exists to enforce.
* tools_import_export_servers: only 'save' was gated; 'get_servers' and
  'load_servers' relied on @pga_login_required alone. Lower impact than
  the above: get_servers only returns the calling user's own servers,
  and load_servers only previews an already-uploaded file without
  persisting (the actual write stays gated at 'save').
* tools_search_objects: only 'search' was gated; 'types' (a static list
  of supported object-type names for the dropdown, no DB content) was
  not.
* change_password: the self-service password-change route
  (browser.change_password) never checked this permission, so revoking
  "Change Password" from a role in Role management had no effect --
  users under that role could still change their own password. Lower
  severity than the others since the route is inherently self-scoped
  (can only ever affect the caller's own password).

Extends tools/tests/test_tool_permissions_required.py with 17 new
scenarios for the first three, and adds a dedicated
browser/tests/test_change_password_permission_required.py for the last
(different blueprint, different permission category).

Verified against a live PostgreSQL server in SERVER mode: all 40
scenarios (37 + 3) pass with the fix applied. Negative control:
reverting the four fixes reproduces exactly the reported gaps -- 17 of
the 20 pre-existing scenarios still pass unaffected, the 17 new ones
fail, and change_password returns 200 instead of 403 -- confirming the
tests actually catch the regression rather than passing vacuously.
2026-07-27 05:28:57 +00:00
Ashesh Vashi 84b93c268d chore(deps): consolidated dependency bumps (setuptools, fast-uri, tar, shell-quote, svgo) (#10199)
Supersedes 7 open Dependabot PRs by applying the safe ones and
properly fixing the one with a broken lockfile, in one CI cycle:

- setuptools ==82.* -> ==83.* (#10144, #10145 - duplicate PRs, same
  patch). Only touches the `python_version > '3.9'` line; the
  `<82; python_version <= '3.9'` gate for Python 3.9 is untouched,
  so this doesn't affect Python 3.9 support.
- fast-uri 3.1.2 -> 3.1.4 in both /web and /runtime (#10183, #10195)
  - fixes two real CVEs (GHSA-v2hh-gcrm-f6hx, GHSA-4c8g-83qw-93j6).
- tar 7.5.16 -> 7.5.21 (#10182) - patch series, decompression-bomb
  and unbounded-recursion hardening only.
- shell-quote 1.8.4 -> 1.10.0 (#10185) - additive opt-in option +
  parser fixes, no breaking changes.
- svgo 3.3.3 -> 4.0.2 (#10184) - Dependabot's own PR left yarn.lock
  internally inconsistent (dropped the workspace-level `svgo` entry
  while merging version-range blocks), so `yarn install --immutable`
  failed in CI with "the lockfile would have been modified by this
  install". Regenerated properly via `yarn up`/`yarn dedupe` here.
  The direct `svgo`/`svgo-loader` deps are not actually wired into
  any webpack rule (verified via grep) - the real SVG pipeline is
  `@svgr/webpack` -> `@svgr/plugin-svgo` -> svgo 3.3.3, which this
  bump does not touch - so the major version jump has no build
  impact. `yarn.lock` now correctly keeps that separate 3.3.3
  resolution alongside the deduped 4.0.2 one.

Not included (structurally blocked, tracked separately):
- paramiko 3.5.1 -> 5.0.0 (#9927): paramiko 5 removes DSSKey
  entirely; sshtunnel 0.4.0 (dormant since 2021) still references
  paramiko.DSSKey, so `import sshtunnel` would crash immediately.
- pywinpty 2.0.* -> 3.0.* (#10082, #10084): the existing pin cites
  https://github.com/andfoy/pywinpty/issues/545, confirmed still
  open ("process read and write not working as expected in 3.x").

Verified: `yarn install --immutable` clean in both /web and
/runtime, `yarn run linter` clean, full `yarn run bundle:dev`
compiles successfully.
2026-07-25 01:46:48 +05:30
Ashesh Vashi 1e7407bbf6 Merge remote-tracking branch 'origin/master' into cve-9.17-rebase 2026-07-25 01:04:16 +05:30
Ashesh Vashi 9bcc0ff8de docs: fix RST emphasis error in v9.17 release notes
Trailing "SELECT *" broke sphinx's inline emphasis parser (bare *
without matching close), failing the macOS appbundle docs build.
2026-07-25 01:02:36 +05:30
Kundan Sable f75452bfd0 test(security): behavioral regression test for name-literal SQL escaping
Add test_name_literal_sql_escaping.py covering the templates fixed in the
CVE-2026-12044 follow-up: index Statistics (coll_stats.sql, both
dialects), publications (pg + ppas), and subscriptions dependency/
get-position lookups.

Each scenario renders the real template with a stacked-statement
apostrophe payload and asserts (1) the object name appears exactly as
qtLiteral escapes it and (2) the rendered SQL parses as exactly one
statement -- the property that actually prevents statement smuggling.
Verified the semantic assertion fails on the pre-patch raw-interpolation
form (parses as 2 statements) and passes on the fixed form, so the test
genuinely guards the fix rather than trivially passing.

Complements test_stats_template_regclass_cast.py (single-index
pgstatindex path) and the lint guard in test_sql_string_literal_lint.py.
Pure template-render test, no DB required.

Also independently verified during review: the fix's escaping neutralises
a live stacked-statement injection on PostgreSQL 16 (pre-patch slept 5s,
post-patch 0.0s) for both index-stats and pub/sub paths, and confirmed
the browser tree label and Statistics grid render object names as
React-escaped JSX text, so the HTML/XSS probe in an object name does not
execute in those paths. EPAS/ppas runtime path not exercised (ppas
publication templates are byte-identical to the pg variants and are
covered at render level by this test).
2026-07-25 00:55:46 +05:30
Kundan Sable 73b3218992 fix(security): escape index-stats/publication/subscription name sinks (CVE-2026-12044 follow-up)
The 9.16 fix for CVE-2026-12044 hardened qtLiteral and switched sixteen
COMMENT ON / pgstattuple / pgstatindex templates to it, but the fix
missed several sinks that were previously excused in
test_sql_string_literal_lint.py's ALLOWLIST on the (incorrect)
assumption that schema/table/publication/subscription names sourced
from pg_catalog via the browser tree could never contain apostrophes.
They can, since PostgreSQL permits arbitrary characters in quoted
identifiers, so a low-privileged user able to CREATE TABLE, CREATE
PUBLICATION, or CREATE SUBSCRIPTION can plant a name that breaks out of
the unescaped '{{ name }}' interpolation once any user views that
object's Statistics or Dependencies tab.

Switch the following templates to qtLiteral(conn):
- schemas/tables/templates/indexes/sql/{16_plus,default}/coll_stats.sql
  (Index Statistics -- reported as "SQL injection in pgAdmin index
  Statistics (incomplete fix for CVE-2026-12044)")
- publications/templates/publications/{pg,ppas}/default/sql/
  {dependencies,get_position}.sql
- subscriptions/templates/subscriptions/sql/default/
  {dependencies,get_position}.sql

publications/__init__.py and subscriptions/__init__.py now pass
conn=self.conn into the dependencies.sql render_template call so the
qtLiteral filter has a connection to quote against. Remove the
corresponding ALLOWLIST entries in test_sql_string_literal_lint.py now
that these sinks are properly escaped instead of merely assumed safe.

Patch supplied pre-written; manual testing by the reporter reported
both the SQL-injection and HTML-injection probes as positive, but the
exact pre/post-fix state tested was not fully disambiguated in this
session -- treat as reported-but-not-independently-verified pending
follow-up confirmation.
2026-07-25 00:55:46 +05:30
Dave Page 24fdcf0f58 fix(security): add missing auth decorators to Constraints/preferences/debugger/schema_diff routes (CVE-2026-12046 follow-up)
In SERVER mode pgAdmin enforces authentication per route via
@pga_login_required; the before_request hook only handles desktop
auto-login and the Kerberos/Webserver redirect, so a route shipped
without the decorator is reachable unauthenticated (CWE-306). This is
the same defect class as CVE-2026-12046 (the sqleditor close/
update_connection routes).

A sweep found further omissions, now fixed with @pga_login_required:

* Constraints blueprint: nodes, proplist (obj), delete -- the routes
  named in the "SQL injection in pgAdmin index Statistics" follow-up
  report as an incomplete fix for CVE-2026-12046. delete is a
  state-mutating DELETE (removes table constraints); nodes/proplist
  return object information. Adds the missing pga_login_required import
  to this module.
* preferences.get_all_cli (GET)
* debugger.close (DELETE)
* schema_diff.close (DELETE)
2026-07-25 00:55:46 +05:30
Ashesh Vashi e7a8576731 Fix MASTER_PASSWORD_HOOK tokenisation on Windows
posix=False in shlex.split() never strips quote characters, so any
Windows hook path quoted to handle spaces (or any quoted argument)
came out with literal quotes still attached, and unquoted spaced
paths were split into multiple argv elements either way.

Use a shlex.shlex instance with posix=True (correct quote-stripping)
and escape='' (so backslashes in Windows paths are not treated as
escape characters). Document the quoting requirement in config.py
and add regression tests for quoted/unquoted spaced paths.
2026-07-25 00:55:46 +05:30
Dave Page ea7e798aac Fix OS command injection in the MASTER_PASSWORD_HOOK feature
The MASTER_PASSWORD_HOOK setting lets administrators specify an external
command that returns a per-user encryption key, with %u in the configured
string replaced by the current user's name. The previous implementation
substituted the username into the command string and executed the result
with subprocess.Popen(..., shell=True). Because the username can originate
from an external authentication source (OAuth/OIDC claims, Kerberos,
webserver auth), a username containing shell metacharacters allowed an
authenticated user to execute arbitrary commands as the pgAdmin service
account in deployments where the hook uses %u.

Tokenise the trusted hook string into an argument vector first, substitute
the untrusted username into the individual arguments, and execute with
shell=False. The username is therefore always confined to a single argv
element and any shell metacharacters it contains are inert.

Note for administrators: hooks that previously relied on shell features
(pipes, redirection, environment-variable expansion, globbing) in the
MASTER_PASSWORD_HOOK string itself will no longer have those interpreted;
such logic should be moved into the hook script. The documented form,
'<PATH>/script.sh %u', is unaffected.

Adds regression tests covering usernames containing ';', '$()', backticks,
pipes, '&&' and newlines, plus an end-to-end marker-file proof that no
shell execution occurs.

Reported-by: B1gN0Se
2026-07-25 00:55:46 +05:30
Ashesh Vashi 64a9cdbd6a fix: close remaining tool-RBAC gaps found in review
Extends the tool-permission enforcement pattern from the reported CVE
fix to the backup/restore/maintenance/import_export blueprints, which
had the identical front-door-only gap: only create_*_job was gated,
while objects()/check_utility_exists()/get_import_export_settings()
relied on @pga_login_required alone.

Also clears password/save_password/tunnel_password on adhoc server
clones, not just ownership fields - Server.clone() copies every
column, so a non-owner cloning another user's shared server was still
persisting that user's stored credentials into a row the non-owner
now owns.

Adds missing socket test scenarios (erd tables, psql socket_input/
socket_set_role/resize) that the existing test docstring claimed to
cover but didn't.
2026-07-25 00:55:46 +05:30
Dave Page ba1984718a Add RBAC regression tests for tool routes, sockets and adhoc ownership
Add regression coverage for the authorisation fixes:

  * test_tool_permissions_required: a consolidated, per-blueprint check
    that logs in as a user with no roles (hence no tool permissions) and
    asserts every gated backend HTTP route across the query tool, grant
    wizard, schema diff, ERD, PSQL and debugger returns 403. This catches
    any future route added to these blueprints without the decorator.
  * test_tool_socket_permissions_required: asserts the schema diff
    compare_database/compare_schema and psql start_process Socket.IO
    handlers refuse a user lacking the tool permission.
  * test_adhoc_connect_server_ownership: asserts that an adhoc connect
    triggered by a non-owner against an administrator-owned shared server
    persists a server row owned by the caller and not shared.

All three are skipped in DESKTOP mode, where every request is
auto-authenticated as the all-permissions DESKTOP_USER.
2026-07-25 00:55:45 +05:30
Dave Page a7e74a6ed6 Re-home adhoc cloned servers to the calling user
When /misc/workspace/adhoc_connect_server is given a sid, it clones the
existing server. Server.clone() copies every column of the source row,
including user_id, shared and shared_username. When a non-owner triggered
an adhoc connect against an administrator-owned shared server, the clone
inherited the administrator's ownership and shared flag, so pgAdmin
persisted a new, administrator-owned, shared adhoc server row created at
the behest of another user; the row is committed before the connection is
attempted, so it survived even when the connection failed.

Force the cloned adhoc record to belong to the current user and to be
private (user_id, shared, shared_username) before committing, mirroring
the new-server branch, so a non-owner can no longer persist a
cross-tenant server record.
2026-07-25 00:55:45 +05:30
Dave Page d36bd8dc96 Enforce tool RBAC on every backend route, not just the entry point
The tool permissions (tools_query_tool, tools_grant_wizard,
tools_schema_diff, tools_erd_tool, tools_psql_tool, tools_debugger) were
enforced only on a single "front door" route per tool, whilst the rest of
each tool's backend workflow relied on pga_login_required alone. An
authenticated user who had been denied a tool could therefore still drive
the tool through its other routes and Socket.IO handlers:

  * Query Tool: View/Edit Data via sqleditor.initialize_viewdata and the
    rest of the view-data chain, bypassing the gate on initialize_sqleditor.
  * Grant Wizard: object discovery (objects), SQL preview (modified_sql)
    and the actual privilege change (apply) were ungated; only acl_list
    was protected. This allowed real GRANTs to be generated and applied.
  * Schema Diff: initialize, servers, get_server, connect_server,
    connect_database, databases, schemas, ddl_compare and the
    compare_database/compare_schema socket handlers were ungated; only
    panel was protected.
  * ERD: initialize, prequisite, sql (table DDL generation) and the
    tables socket handler were ungated; only panel was protected.
  * PSQL: the panel route and the entire /pty Socket.IO namespace
    (start_process, socket_input, socket_set_role, resize) had no tool
    permission check at all, so a denied user could still obtain an
    interactive psql session.
  * Debugger: the directly addressable get_arguments/set_arguments/
    clear_arguments routes were ungated.

Apply permissions_required to the HTTP routes and socket_permissions_required
to the Socket.IO handlers so the tool permission is enforced consistently
across each tool's surface. The permission check is the outermost
decorator, so it runs before any connection or transaction lookup.
2026-07-25 00:55:45 +05:30
Dave Page 461c3afba9 Add socket_permissions_required decorator for Socket.IO RBAC
Flask-Security's permissions_required only guards HTTP routes; pgAdmin's
Socket.IO event handlers had no permission-aware equivalent and relied on
socket_login_required, which checks authentication but not the tool
permission. Add socket_permissions_required as the socket counterpart of
permissions_required so that event handlers can enforce the same
tool-level RBAC as the routes.

It reads the user's permissions via has_permission() rather than
flask_principal's Permission().can(), so it does not depend on the
principal identity having been loaded onto the socket request context,
and it honours pgAdmin's Administrator bypass. On failure it disconnects
and raises ConnectionRefusedError, mirroring socket_login_required.
2026-07-25 00:55:45 +05:30
Ashesh Vashi 54ff7b4dce Updated message catalogs for v9.17 2026-07-25 00:54:55 +05:30
Ashesh Vashi 5d39a52a73 docs: add release notes for v9.17 2026-07-25 00:21:27 +05:30
Niv Greenstein 108797d48a feat: add custom tile provider settings for Geometry Viewer (#10142)
Geometry Viewer's base-layer choices were hardcoded (Empty, Street,
Topography, Gray Style, Light Color/Dark Matter), with no way to
point at a private/internal tile server or a provider not in that
built-in list.

Add a geometry_viewer preferences category (custom_tile_url, _name,
_crs, _attribution, _max_zoom) under sqleditor. When a valid custom
tile provider URL is configured (must be http(s) and contain {x},
{y}, {z}), it's used as the default base layer; built-in tiled
layers are hidden unless the custom provider's CRS is also
EPSG:3857 (Web Mercator), since they can't be reprojected together.
Non-mercator custom providers (e.g. EPSG:4326) only apply to SRID
4326 geometry data; other SRIDs still render on the existing blank
Cartesian plane.

Custom name/attribution are DOMPurify-sanitized before being passed
to Leaflet (both are innerHTML sinks). URL is not sanitized but is
gated by the http(s):// prefix check, so javascript:/data: URLs are
rejected; it's consumed as a tile-image src template, not an HTML
sink.

Adds GeometryViewerUtils.spec.js covering URL validation, defaults,
sanitization, CRS resolution/fallback, and base-layer composition.
2026-07-24 22:17:51 +05:30
Ashesh Vashi 7339eb8dcc fix: report the correct error message on trigger node.sql failure (#10197)
The failure path for the second query (node.sql, resolving the
trigger function's node data) reported errormsg=rset — the result
dict from the first, already-successful get_function_oid.sql query
— instead of res, the actual error from the failed query. Any
failure here (permissions, catalog access) surfaced a stale,
misleading payload instead of the real cause.
2026-07-24 21:59:06 +05:30
Ashesh Vashi 34108aa50f fix: remove unused sqlalchemy exists import in server_access.py (#10196)
'exists' was imported but never called; the .exists() at line 165 is
the query-builder method on a Query instance, not this function.
2026-07-24 21:58:00 +05:30
lkmatsumura 1e1240038a refactor(server_groups): centralize shared-group visibility and access logic (#10077)
Replace the per-server ServerGroupModule.has_shared_server() loop and
scattered ad hoc shared-server checks with two centralized helpers,
get_server_groups_for_user() and get_server_groups_for_user_query(),
in server_access.py — a single SQL EXISTS query instead of iterating
servers per group. get_server_group() reuses the same shared-access
logic, and returned groups now carry is_shared_group metadata so the
browser tree can render the correct icon and derive edit/delete
permissions consistently.

update() now resolves the target group via get_server_group(gid,
hide_shared=True), so a non-owner can no longer rename another
user's shared group; delete() already blocked deleting shared or
first-user groups and now reports clearer errors for both cases.
Desktop mode's non-SERVER_MODE query path uses
literal(0).label('is_shared_group') (a bare int has no .label()).

Adds test_sg_permissions.py covering owned/shared/desktop group
visibility and access.
2026-07-24 19:11:27 +05:30
Hari Prasad 966a8ae99a docs: add basic starter contributing guide (#10093)
* docs: add basic starter contributing guide

Add a CONTRIBUTING.md file to document the contribution workflow for pgAdmin 4.

Includes:
- Getting started instructions
- Issue reporting guidelines
- Development workflow and coding practices
- Pull request requirements
- Development environment setup
- Database migration guidance
- Security reporting process
- Community support resources

This provides a single reference for new and existing contributors.

* add language tag on the opening fence of the Windows command block

* Add architecture overview, define $PGADMIN4_SRC, and link GitHub URLs

* docs: update contribution guidance

Clarify contributor workflow, fix the runtime link, and add references to existing contributor docs.
2026-07-24 19:05:11 +05:30
Dave Page 75dd304d25 fix(cloud): stop googleapiclient pulling in the system oauth2client (#10110) (#10112)
On packaged Linux installs the bundled venv is created with
--system-site-packages (issue #7173) so it can reach system packages such
as dbus-python. That also exposes the deprecated system oauth2client and
whatever pyOpenSSL ships alongside it. googleapiclient imports oauth2client
optionally, and on Ubuntu 24.04 that drags in a pyOpenSSL too old for our
bundled cryptography, aborting startup at blueprint registration with
"AttributeError: module 'lib' has no attribute 'GEN_EMAIL'".

pgAdmin only ever authenticates to Google via google-auth and
google-auth-oauthlib, so park a None sentinel under
sys.modules['oauth2client'] before importing googleapiclient in both the
web module and the standalone pgacloud provider. The optional import then
fails cleanly and googleapiclient falls back to google-auth.

Closes #10110
2026-07-24 19:02:29 +05:30
n0099 67755809a6 zh_Hans_CN translate: truncate 截断 -> 清空 (#10138)
this also sync with phpmyadmin: https://github.com/phpmyadmin/phpmyadmin/blob/c605fd63bc2e48b682e06d6d1041de544ddf861c/resources/po/zh_CN.po#L12154
2026-07-24 19:01:55 +05:30
Kobi Hikri e65edd4669 Pin sonarqube-scan-action to a full commit SHA (v8.2.1) (#10154) 2026-07-24 19:01:03 +05:30
Kundan 9bf83b4489 fix: return Response as-is and pass conn when expanding a trigger's function (#10177)
Expanding a Trigger node under a Table to view its function threw
'Response' object is not iterable, crashing the tree.

get_children_nodes() can legitimately return a Flask Response instead
of a list (e.g. gone("Could not find the specified trigger
function") when the trigger's function has no matching row in the
trigger-function node query, as happens for a genuine internal-
language function such as suppress_redundant_updates_trigger).
NodeView.children() / PGChildNodeView.children() unconditionally
sorted whatever get_children_nodes() returned, crashing on a
Response instead of passing it through.

Separately, TriggerView.get_children_nodes() rendered the trigger-
function node.sql template (which quotes fnid via qtLiteral(conn))
without passing conn into the template context. This call site
predates qtLiteral requiring a connection; after qtLiteral was
hardened in 658bb585d to raise ValueError instead of silently
degrading when conn is missing, this broke node.sql rendering for
every trigger, not just the internal-language case.

Fix both: check isinstance(children, flask.Response) in children()
before sorting/iterating and return it as-is, and pass
conn=self.conn into the node.sql render call, matching every other
render_template call site in the file.

Fixes #10117
2026-07-24 19:00:31 +05:30
Regina Obe 58e5fc7aed Fix for #10187 (#10188)
Extension UI gives error with PG 19
column reference "comment" is ambiguous LINE 5: e.comment

Issue caused because in pg19 :

SELECT * FROM pg_catalog.pg_available_extensions();

now outputs an additional column `location`
after default_version and comment is now 4th column
2026-07-24 18:38:40 +05:30
Ashesh Vashi 577a518fcb fix: scan all Mach-O binaries for bundle linkage, not just .so/.dylib (#10189)
_verify_bundle_linkage's host-library scan only matched files by
.so/.dylib name suffix, so a shipped executable (Contents/MacOS/*)
or a Python.framework payload with a host-linked dependency but no
matching suffix could slip through unchecked — the same class of
bug #10135 was added to catch.

Detect Mach-O executables/libraries by content via `file` instead
(same pattern the codesign step in this file already uses),
regardless of extension or executable permission bits, so readable
non-executable dylibs are covered too.
2026-07-24 18:34:03 +05:30
Dave Page 1884356245 fix: fail the macOS build if any bundled library links outside the bundle (#10135)
9.16 failed to start on Intel Macs (#10123): cryptography had no
prebuilt Intel macOS wheel, pip compiled it from source, and its
openssl-sys build linked the builder's Homebrew OpenSSL into
_rust.abi3.so instead of the bundled one. _fixup_imports deliberately
skips _rust.abi3.so, so the dangling reference shipped unnoticed and
the app died on startup for anyone without that external dylib.

Add _verify_bundle_linkage, run after the bundle is assembled and
relocated but before code-signing: walks every .so/.dylib and fails
the build if any install-name points at a build-host prefix
(/usr/local, /opt/homebrew, /opt/local, $SLAVE_HOME). OS libraries
and @loader_path/@rpath/@executable_path references pass untouched.
Turns this class of bug into a build-time failure instead of a
runtime one.
2026-07-24 18:22:08 +05:30
Kundan b85fb6c650 fix: use pg_depend ownership instead of name guessing for SERIAL detection (#10167)
Column-owns-sequence was detected by guessing the sequence name as
<table>_<col>_seq and string-matching it in nextval(...). Renaming
a table/column broke the guess (false negative, #10100); an
unrelated sequence with the same guessed name broke it the other
way (false positive, #10101).

Check col.get('seqrelid') (real pg_depend ownership) instead, and
exclude identity columns. SERIAL is now emitted iff a genuine
ownership dependency exists, independent of naming.

Fixes #10100, #10101
2026-07-24 18:15:20 +05:30
Kundan 5ff4f694fd fix: Schema Diff filter chip toggle broadcasting stale empty selection (#10172)
selectFilterOption() computed newOptions inside the functional
setSelectedFilters(prev => {...}) updater, then used that same
variable to dispatch TRIGGER_CHANGE_FILTER synchronously. Since the
updater runs lazily on next render, every chip toggle broadcast an
empty filter, so the tree always showed "No difference found" until
a full re-Compare.

Compute the new selection synchronously from current state and use
it for both the state update and the dispatched event.

Fixes #10102
2026-07-24 18:14:22 +05:30
hiteshjambhale f9935b3da5 fix: honor selected EOL sequence when copying query text to clipboard (#10160)
getSelectionFromState() joined ranges with the chosen EOL but sliced
each range via state.sliceDoc(), which always uses the document's
own \n regardless of the LF/CRLF preference. Introduced in #8691
when the copy path switched from doc.sliceString(..., lineSep) to
sliceDoc(...) to fix multi-cursor copying.

Slice with state.doc.sliceString(range.from, range.to, lineSep) so
the chosen EOL applies within a range too; the #8691 multi-cursor
join behavior is unchanged.

Fixes #10158
2026-07-24 18:13:27 +05:30
Dave Page cec1b38b2f fix: warn when OAuth2 provider settings are misplaced at the top level of config (#10113)
OAuth2 provider settings are only read from the OAUTH2_CONFIG list;
bare top-level keys like OAUTH2_CLIENT_ID or
OAUTH2_SSL_CERT_VERIFICATION are silently ignored. This is an easy
trap in container deployments where every other option is set via
an individual PGADMIN_CONFIG_<KEY> env var.

Add warn_on_misplaced_oauth2_config(), called once at OAuth2 module
startup, that logs when per-provider OAUTH2_* keys exist at the top
level with no provider configured in OAUTH2_CONFIG. No behavior
change for correctly configured or unconfigured deployments.
Documents the supported container approach in oauth2.rst.

Closes #10053
2026-07-24 18:12:22 +05:30
Kundan 6f8be955a1 fix: render disconnect tree label via className, not embedded HTML (#10169)
Disconnect handlers in server.js/database.js built the transitional
tree-node label as an HTML string and passed it to setLabel(), which
writes via label.textContent — correctly escaping markup, so the
literal "<span class='text-muted'>[Disconnecting...]</span>" text
was shown instead of a greyed-out label.

Pass plain text plus a className: 'text-muted' instead; setLabel()
applies the className to the label span (default file-name class
otherwise), so muted styling comes from CSS, not injected markup.

Fixes #10106
2026-07-24 18:11:22 +05:30
Kundan 49e955a855 feat: add preference for default View Data row limit (#10170)
The unqualified "View Data" action always ran with no LIMIT, unlike
"First/Last 100 Rows" which hardcode limit=100 — unusable on large
tables with no way to cap it.

Add view_data_default_row_limit preference (int, default 0 =
unlimited) under sqleditor Options. GridCommand.__init__ applies it
as self.limit when cmd_type == VIEW_ALL_ROWS and the value is
positive; objectquery.sql already renders LIMIT only when > 0, so
no template changes needed. First/Last 100 Rows untouched.

Closes #10104
2026-07-24 18:10:13 +05:30
Kundan 6ad6024bca fix: detect a selected-but-unusable OS keyring and fall back gracefully (#10168)
evaluate_and_patch_config() only disabled USE_OS_SECRET_STORAGE when
keyring.get_keyring().name == 'fail Keyring'. On Debian 13 over RDP, a
real backend (e.g. SecretService) is selected because no D-Bus/GNOME
Keyring session is running, so the name check passes even though every
actual keyring call fails — leaving the crypt key unset and every
operation raising a bare CryptKeyMissing.

Probe the selected backend with a harmless read of a never-existing
entry after selection. A healthy backend returns None without
prompting; an unusable one raises, and we disable
USE_OS_SECRET_STORAGE so the app falls back to the master-password /
in-app crypt key mechanism instead of failing outright.

Fixes #10107
2026-07-24 18:08:27 +05:30
Kundan aeef9c2d80 fix: ALT+F5 not falling back to statement under cursor (#10173)
triggerExecution() only fell back to getQueryAt(cursor) when
getSelection() was falsy. Since #7293/#8691 (non-continuous
highlighted-block support), getSelection() flattens and joins all
ranges, so multiple empty cursor ranges can produce a truthy but
whitespace-only string (e.g. "\n"), skipping the fallback entirely —
ALT+F5 silently tried to execute whitespace. Same stale check existed
in checkUnderlineQueryCursorWarning(). Also fixes a latent
state.selection.head reference (EditorSelection has no such
property; correct field is state.selection.main.head).

Treat a whitespace-only selection as empty before the fallback check
in both functions. Real highlighted selections always contain
non-whitespace, so #7293/#9570 behavior is unaffected.

Fixes #10109
2026-07-24 18:07:26 +05:30
Ashesh Vashi 8812025a8a chore(deps): Bump patch/minor dependency bumps (#10176)
Consolidates 20 open dependabot PRs (JS + Python) into one bump, applied
directly rather than cherry-picked (PR branches were stale and would have
reverted unrelated fixes like the yarn packageManager pin). Adds further
same-major patch/minor bumps found by auditing beyond dependabot's own PR
list.

JS (web + runtime): axios, brace-expansion, form-data, undici, js-yaml,
dompurify, @babel/core, webpack, sharp, electron, eslint, react-checkbox-tree,
autoprefixer, eslint-plugin-jest, globals, jest, jest-environment-jsdom, svgo,
terser-webpack-plugin, typescript-eslint, webpack-bundle-analyzer,
@date-io/date-fns, @szhsin/react-menu, @tanstack/react-query, @types/react,
ajv, anti-trojan-source, ip-address, marked, moment-timezone, papaparse,
postcss, react, react-dom, react-draggable, react-timer-hook, sql-formatter,
zustand.

Python: certifi, selenium (version-gated: 4.45.0 requires Python >=3.10,
4.44.0 kept for <=3.9 to preserve Python 3.9 support).

Reverted / excluded, with reasons:
- azure-mgmt-resource 26.0.0: moved ResourceManagementClient from
  azure.mgmt.resource to azure.mgmt.resource.resources, breaking
  pgadmin/misc/cloud/azure/__init__.py at import time. Caught by the Python
  regression suite. Kept at 25.0.0.
- @simonwep/pickr 1.10.0: switched its build tool to tsup, which marks its
  UMD bundle as an ES module via Symbol.toStringTag instead of the
  __esModule flag Babel's interop helper checks for. Babel double-wraps the
  export, so `new Pickr(...)` resolves to a non-constructor and crashes
  every dialog that mounts a color picker -- reproduces only in the
  production/minified webpack build, not the dev bundle or Jest. Pinned to
  ~1.9.1 (tilde, not caret) so a future install can't silently float back to
  1.10.x.
- paramiko 3->5 (#9927): structurally blocked by sshtunnel 0.4.0 still
  referencing paramiko.DSSKey.
- @mui/material / @mui/x-date-pickers 7/8->9 (#10091, #10092): known
  UI-breaking, needs the accompanying component fixes tracked on a separate
  branch, not a bare version bump.
- react-arborist, @tanstack/react-virtual, react-frame-component: same-major
  bumps available but excluded -- core object-browser tree, already-fragile
  virtualization code, or tilde-pinned range respectively.
- A handful of JS packages hit Yarn 4.15's registry quarantine gate (blocks
  just-published versions); backed off to the next-older version instead of
  forcing through.

Added core-js as an explicit devDependency: it was never declared despite
webpack's Babel config (useBuiltIns: 'usage', corejs: 3) requiring it --
it only worked because @simonwep/pickr 1.9.1 happened to pull it in
transitively, which broke when pickr was briefly bumped.

Verified: eslint (web + runtime), full JS test suite (149/149 suites,
916/916 tests), webpack production build compiles clean, Python regression
suite (2388/2388, excl. Selenium), Selenium feature_tests (17/19 pass; the
2 failures trace to a local pldbgapi-extension gap, unrelated to any
bumped dependency).
2026-07-23 15:28:38 +05:30
Ashesh Vashi 1993109475 fix: pin Yarn version from packageManager field in remaining build scripts (#10175)
#10156 fixed CI build failures caused by Yarn 4.x fetching a newer patch
with different builtin compat hashes, breaking --immutable lockfile
validation, but only patched pkg/linux/build-functions.sh and the GHA
workflows. Make.bat, pkg/mac/build-functions.sh, pkg/pip/build.sh, and
Dockerfile still hardcoded 'yarn set version 4', which is what broke the
Windows Jenkins snapshot build (job 1669).
2026-07-23 13:06:29 +05:30
Ashesh Vashi b15c745dc1 fix: pin Yarn version from packageManager field in build scripts (#10156)
* fix: pin Yarn version from packageManager field in build scripts

Replace hardcoded 'yarn set version 4' in build-functions.sh with a
dynamic lookup from each workspace's package.json packageManager field.
Also syncs runtime yarn version to 4.15.0 to match web/package.json.

Fixes CI build failures caused by Yarn 4.x fetching a newer patch that
produces different builtin compat hashes, breaking --immutable lockfile
validation.
2026-07-16 11:48:36 +05:30
Luiz K Matsumura dfc4ef37d0 refactor: clarify ServerGroup relationships to Server and SharedServer (#10076)
The Server and SharedServer models each declared a relationship named
'servers' that actually pointed at the parent ServerGroup, with backrefs
('server'/'sharedserver') providing the collections. The naming was the
inverse of what it modelled and made the call sites read oddly.

Redefine the relationships in the natural direction using explicit
back_populates: ServerGroup gains 'servers' and 'sharedservers'
collections (carrying the existing delete-orphan cascade), and Server and
SharedServer each gain a 'servergroup' reference. The two call sites that
read the group name are updated from server.servers.name to
server.servergroup.name accordingly.

This is a naming/modelling cleanup with no functional or schema change, so
no migration is required.
2026-06-19 10:32:02 +01:00