AEV-2026-0028
Postgres MCP Pro restricted mode allow list does not reach a function called in a FROM clause (CVE-2026-85620)
Postgres MCP Pro's SafeSqlDriver blocks a dangerous PostgreSQL function by name whenever pglast represents the call as a FuncCall node, and this entry confirmed that block directly by running the real validator. The identical function called inside a FROM clause parses as a RangeFunction node whose functions attribute nests the same FuncCall one further tuple deep, a container the validator's own recursive walk never opens, so the same allow list check never runs. Formally assigned CVE-2026-85620 on 4 September 2026, with a proposed fix, pull request 200, open and unmerged as of this entry.
Affected
- Organisation
- The crystaldba postgres-mcp project (Crystal DBA)
- Product
- Postgres MCP Pro
- Component
- SafeSqlDriver._validate_node and _validate in src/postgres_mcp/sql/safe_sql.py, exercised only when the server is run in restricted access mode
- Versions
- Through version 0.3.0, confirmed directly by executing the affected validator at the v0.3.0 tag; independently confirmed still present, unpatched, at the project's current default branch head, commit 15c8e33353546148acc2d8bd784551cf3905d1e2; no patched version found
- Configurations
- Server started with --access-mode=restricted, confirmed directly against server.py, which wraps the base SQL driver in SafeSqlDriver only under this flag; The connected PostgreSQL role holds the pg_read_server_files privilege or superuser status, the precondition the primary issue states for the file read functions to succeed once the query reaches PostgreSQL
Execution authority facts
Fields the evidence does not establish are shown as unknown rather than guessed.
- Intended task
- An MCP client or agent runs a SQL query through Postgres MCP Pro's restricted mode, which is intended to limit execution to an explicit allow list of function calls
- Agent
- Postgres MCP Pro's own SafeSqlDriver, which parses and validates a query before handing it to PostgreSQL
- Delegating actor
- The operator who deploys Postgres MCP Pro, selects restricted mode specifically to constrain what a connected agent may execute, and configures the PostgreSQL role the connection authenticates as
- Action
- Executed pg_read_file, pg_ls_dir, pg_read_binary_file or pg_stat_file called inside a FROM clause, immediately after this entry confirmed the identical call in ordinary expression position is correctly rejected by the same validator
- Target resource
- Files on the PostgreSQL server's own filesystem reachable by the process, gated only by whatever the connected role's own pg_read_server_files or superuser privilege separately permits
- Environment
- Wherever the operator deploys Postgres MCP Pro and its connected PostgreSQL server
- Credentials used
- The PostgreSQL role the restricted mode connection authenticates as
- Privileges available
- Whatever server side file access that role's own pg_read_server_files or superuser privilege grants once a query reaches PostgreSQL itself
- Authority presented
- A syntactically valid SQL query naming an otherwise disallowed function inside a FROM clause rather than in the ordinary expression position the same validator already checks
- Authority required
- Authorization for the specific function invoked, independent of which grammar position within the query the function name appears in
- Applicable policy
- SafeSqlDriver's ALLOWED_FUNCTIONS check, confirmed directly to correctly block pg_read_file, pg_ls_dir, pg_read_binary_file and pg_stat_file when pglast represents the call as a FuncCall node reached by the validator's own recursive walk; not reached when the identical FuncCall is nested inside a RangeFunction node's own functions attribute
- Approval mechanism
- none
- Required approver
- unknown
- Independent approval
- no
- Action binding
- The allow list check is bound to a FuncCall node the recursive walk visits directly; it is not bound to the semantically identical FuncCall this entry confirmed sits one tuple deeper inside a RangeFunction's own functions attribute
- Sequence context
- Validation runs once, inside _validate, immediately before execute_query hands the query to PostgreSQL with force_readonly forced to true; there is no later checkpoint that re-examines which functions the query actually invokes
Impact
- Consequence
- File contents returned by pg_read_file, pg_ls_dir, pg_read_binary_file and pg_stat_file called through the bypass path, confirmed directly by this entry running the real, unmodified SafeSqlDriver from the affected v0.3.0 source against all four functions, with a CREATE TABLE write control confirmed still blocked throughout
- Reach
- organisational
- Reversibility
- unknown
- Detectability
- unknown
- Propagation
- A concrete propagation path exists, for example reading a credential bearing file the PostgreSQL server process can access, but this entry did not observe the effect actually spreading to a further system
- Recovery
- No patched version found; v0.3.0 remains the project's latest tag as of this entry. Pull request 200 proposes the missing traversal step; this entry applied and ran that exact diff and confirmed it blocks the bypass while still permitting an allow listed FROM clause function, unnest. It is open and has not merged as of this entry.
Evidence
Primary sources
- Postgres MCP Pro through 0.3.0 contains a restricted mode bypass permitting arbitrary server side file read via a FROM clause function (CVE-2026-85620) (GitHub Advisory Database)
Supporting sources
- [Security] Restricted (read only) mode bypass: arbitrary server side file read via FROM clause function (crystaldba/postgres-mcp (GitHub Issues))
- fix: Block dangerous functions in FROM clause (RangeFunction allowlist bypass) #178 (crystaldba/postgres-mcp (GitHub Pull Requests))
- safe_sql.py at the v0.3.0 tag (SafeSqlDriver._validate_node and _validate) (crystaldba/postgres-mcp (GitHub, source))
- server.py at the project's current default branch (AccessMode and get_sql_driver) (crystaldba/postgres-mcp (GitHub, source))
- safe_sql.py at the project's current default branch head, commit 15c8e33 (crystaldba/postgres-mcp (GitHub, source))
- Reproduction status
- Independently reproduced by Moona Intelligence. This session downloaded the real, unmodified SafeSqlDriver source at the v0.3.0 tag and executed its own _validate method directly against the blocked and bypassing forms of four distinct dangerous functions, against unnest as an allow listed control, and against a CREATE TABLE write control, with no PostgreSQL connection required. This session separately parsed the bypass query with the same pglast library the validator itself uses, confirming the exact nested tuple structure inside RangeFunction.functions, and applied and ran pull request 200's own diff against the same source, confirming what it closes and what it leaves unaffected.
- Evidence state
- Confirmed
Known unknowns
- Whether this has been exploited against a real Postgres MCP Pro deployment.
- What share of restricted mode deployments connect as a PostgreSQL role holding pg_read_server_files or superuser privilege, as distinct from a role that would be refused by PostgreSQL's own privilege system once the query reached it.
- Which body served as the assigning CVE Numbering Authority for CVE-2026-85620; GitHub's own advisory record attributes publication to the National Vulnerability Database, and this session could not independently confirm the assignment chain since direct fetch of both cve.org and nvd.nist.gov was blocked by this session's network egress policy.
- Whether pull request 200 will merge as currently written or be revised first.
Limitations
- This session's direct fetch of the CVE Program's own record at cve.org and of nvd.nist.gov were both blocked by this session's network egress policy; CVE metadata is corroborated through the GitHub Advisory Database rather than read directly from either.
- The GitHub issue and the pull request were also read through this session's automated page fetch and summarization tool, though every material claim taken from them is independently confirmed against this session's own direct execution of the affected source and its own direct parse of the bypass query.
Claim provenance
- verified
GitHub's Security Advisory Database, read directly, assigns CVE-2026-85620, rates it CVSS 4.0 base 9.2 Critical, and classifies it CWE-863, Incorrect Authorization, against Postgres MCP Pro through 0.3.0.
The Function Was Forbidden. Changing Where It Appeared in the Query Made It Authorized. - verified
This session downloaded the exact affected file and executed its own SafeSqlDriver._validate method directly. SELECT pg_read_file('/etc/passwd') is rejected. SELECT * FROM pg_read_file('/etc/passwd') AS t, SELECT * FROM pg_ls_dir('/etc') AS t, SELECT * FROM pg_read_binary_file('/etc/passwd') AS t and SELECT * FROM pg_stat_file('/etc/passwd') AS t all validate cleanly against the same unmodified validator. SELECT * FROM unnest(ARRAY[1,2,3]) AS t also validates cleanly, as an allow listed function should. CREATE TABLE foo (id int) is rejected.
The Function Was Forbidden. Changing Where It Appeared in the Query Made It Authorized. - independent-reporting
The primary issue, filed independently of the project's maintainers, states the tested PostgreSQL version, the --access-mode=restricted precondition, the pg_read_server_files or superuser privilege precondition, and the same four affected functions this entry independently confirmed by execution.
The Function Was Forbidden. Changing Where It Appeared in the Query Made It Authorized. - verified
This session fetched refs/pull/200/head directly from the project's own repository, resolving to commit 46d4b2e22a520a015e6adeb761dac6a45b4f35d8, applied its diff to the v0.3.0 source and ran the patched validator directly, confirming it blocks the bypass while continuing to permit unnest in the same FROM clause position. Confirmed open and unmerged.
The Function Was Forbidden. Changing Where It Appeared in the Query Made It Authorized. - verified
This session read the project's current default branch head directly through a shallow clone of its own repository, confirmed it carries none of pull request 200's changes, and confirmed by direct execution that the identical bypass still validates cleanly against this current, unreleased state; git ls-remote against the project's own repository confirmed v0.3.0 remains the latest tag.
The Function Was Forbidden. Changing Where It Appeared in the Query Made It Authorized.
