The RangeFunction Blind Spot: How CVE-2026-85620 Broke Postgres MCP's Restricted Mode

Policy | CryptoCube |

The audit revealed a single oversight in the parser's AST traversal, and that oversight bypassed every access control in Postgres MCP Pro's restricted mode. CVE-2026-85620 carries a CVSS score of 9.2. The root cause is not a failure of the PostgreSQL engine itself. It is an engineering gap in the application-layer authorization model.

Specifically, the safe_sql.py module only inspects FuncCall AST nodes. The RangeFunction node — which represents function calls inside a FROM clause — is left unchecked. A query like SELECT * FROM pg_read_file('/etc/passwd') triggers no allowlist validation. The parser only sees the SELECT statement and the column list, missing the dangerous function call nested in the data source.

This is a classic incomplete verification pattern. The restricted mode was designed as a "read-only transaction with allowlist validation" boundary. Operators were told this configuration was safe for production. The documentation claimed safety, but the code only covered one of two possible function call locations. Code does not lie, only the documentation does.

I have audited this type of parser boundary before. During my 2018 static analysis of EtherDelta, I found that reentrancy vulnerabilities existed not in the high-level logic but in the specific sequence of external calls within withdrawal functions. The pattern here is identical: the security control is implemented at the wrong abstraction level, leaving a structural gap that a simple syntax trick can exploit.

CVE-2026-85620 affects all versions up to 0.3.0. The fix PR is still under review at the crystaldba/postgres-mcp repository. Any MCP client or AI agent with access to the restricted mode server can trigger the vulnerability. No authentication is required for the restricted logic itself. An attacker can read system configuration files, credentials, and TLS private keys. This turns a supposedly constrained database interface into an arbitrary file read primitive.

Let me be precise about the attack path. The restricted mode has two layers: the read-only transaction, which is enforced by PostgreSQL's role permissions, and the allowlist, which is enforced by the MCP server's custom Python code. The read-only transaction prevents writes. The allowlist prevents dangerous reads. If the server runs with a role that has the pg_read_server_files permission, the read-only transaction does not block file access. The allowlist is the only remaining barrier.

The barrier fails because the custom AST parser—not the PostgreSQL standard parser—misses the RangeFunction node. The project implemented its own SQL validation instead of leveraging the database's built-in permission system. This is a design decision that prioritizes flexibility over security. The result is a false sense of security. If it cannot be verified, it cannot be trusted. Here, the verification was incomplete.

This incident is not isolated. Three CVEs were disclosed within three weeks: CVE-2026-82526 covering retrieval functions, CVE-2026-85695 covering model serving, and CVE-2026-85620 covering database access. The entire AI infrastructure stack—retrieval, model, database—has security gaps that are being discovered in cascade. The MCP ecosystem has reached 97 million monthly SDK downloads and 28% Fortune 500 production usage, according to public reports. A Department of Defense document noted that MCP adoption is outpacing security model maturity. That assessment is now demonstrably correct.

Here is the contrarian angle. The vulnerability is not in PostgreSQL, nor is it in the MCP protocol specification. The vulnerability exists because middleware developers built a custom security layer that duplicated, rather than delegated to, the database's native authorization model. PostgreSQL already has a robust permission system. If the server runs with a role that lacks pg_read_server_files, the pg_read_file function would fail regardless of the AST parser. The restricted mode's flaw is that it attempted to add an application-level allowlist on top of the database permission system, creating a redundant control that was implemented incorrectly.

The correct architecture is defense in depth. Database-level role permissions should deny file access. The MCP middleware should then add a second layer of validation for business logic, not for basic system access. The current design inverts this: the database role may have the file permission, and the middleware is expected to catch the misuse. This places the entire security burden on a custom Python script that was not designed as a security boundary.

Security is a process, not a feature. The immediate action for operators is clear: remove the restricted mode dependency. Run the MCP server with a PostgreSQL role that does not have pg_read_server_files or pg_read_file permissions. Do not rely on the allowlist. Monitor queries for function calls in the FROM clause. Apply the patch when the fix PR is merged.

The broader lesson for AI agent security is that application-layer controls cannot be treated as the sole defense mechanism. The database engine itself must enforce the security boundary. The MCP layer should validate intent, not re-implement authorization from scratch. A hybrid architecture—database-level permissions plus middleware validation plus network-level client restrictions—is the only reliable approach.

I expect more CVEs in this category. The MCP ecosystem is growing faster than its security tooling. The same parser blind spot may exist in other frameworks. LangChain's tool-calling mechanisms and CrewAI's database integrations may have similar gaps. My confidence in this forecast is based on the pattern: custom parsers for SQL validation are common, and they rarely cover all AST node types.

Will the next disclosure come from a missing SubLink node or an unhandled CommonTableExpr? The question is not if, but when. The code will tell us the answer, as it always does.