Build1 publisher2 min readPublished
One COPY line runs a shell despite AWS's read-only Postgres MCP filter
AWS published CVE-2026-87911, a CVSS 9.6 command injection in its own postgres-mcp-server, where one COPY ... TO PROGRAM line runs a shell on the host. The read-only promise lives in a regex filter that lets the COPY keyword through.
The Engineer · Build desk

What happened
- The read-only enforcement lives in one Python file, mutable_sql_detector.py, built from regex lists of banned keywords and functions plus heuristics for stacked queries.
- The hole reaches only self-managed Postgres where the server connects as a superuser; RDS and Aurora cannot grant the privilege COPY TO PROGRAM needs.
- The same read-only guarantee failed three separate times in six days across Postgres MCP servers.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint A keyword denylist can only block the inputs its authors enumerated, so it cannot bound command execution that Postgres treats as a legal read.
- decision The dependable fix is a database role with no superuser rights and no pg_execute_server_program membership; set that and the filter's gaps stop mattering.
- exposure The exposed surface is every document, ticket or page the agent ingests, because the malicious SQL arrives inside content the model reads.
- precedent One of the week's three affected servers has shipped no fix, so operators running Postgres MCP bridges cannot assume a patch exists.
COPY ... TO PROGRAM is standard Postgres. It pipes the output of a query into a shell command running on the database host [7]. A crafted COPY statement planted in content that an authenticated user's agent processes can run operating-system commands there [2]. The demonstration AWS gives is one line: a SELECT piped to curl, writing a fetched file into /tmp [17].
Read-only mode lets it run. A read-only transaction, BEGIN READ ONLY, stops statements from writing to tables [8]. A PROGRAM clause does its work outside any table, so the transaction lets it run to completion [8].
So the read-only promise lives in the server's own code. The server, AWS Labs' bridge from natural-language tool calls to SQL against Postgres or Aurora, runs read-only unless writes are explicitly enabled, and the enforcement sits in a Python file called mutable_sql_detector.py [3][4]. The package documentation calls that enforcement "best effort" [5]. The source comment is blunter: "This is defence-in-depth. The primary control is operator role permissions" [6].
The record shows these filters missing inputs. On September 4 AWS's own CNA published CVE-2026-85787 and described the defect as "an incomplete list of disallowed inputs": the filter blocked SET but missed set_config(), because "set" is not on a word boundary inside the longer name [14]. The same day, CVE-2026-85620 hit a different vendor's Postgres MCP server, which parsed SQL into a syntax tree but never visited RangeFunction nodes; that project has shipped no fix [15].
By contrast, the database gates COPY ... TO PROGRAM behind a privilege: the role has to be a superuser or a member of pg_execute_server_program [9]. The advisory limits exposure to self-managed Postgres reached over PG_WIRE_PROTOCOL with a role holding one of those privileges [10]. On RDS or Aurora you cannot get that privilege at all, and the advisory says a deployment whose MCP server logs in with a minimal role is unaffected, since the database refuses the command no matter what the regex lets through [11].
The attacker is unauthenticated. According to the advisory, an unauthenticated actor plants the crafted statement in content that is processed when an authenticated user interacts with the server [12]. The CVSS vector records this as PR:N/UI:R, no privileges required and user interaction required [12].
AWS already shipped the fix. Version 1.1.7 landed on PyPI on June 25 with a dedicated COPY PROGRAM guard among a longer blocklist, the pattern blocked even when writes are enabled because it is command execution, not a write [13][16]. The advisory landed on September 9 [1].
What to watch
- Whether the vendor behind CVE-2026-85620 ships an AST fix that actually visits RangeFunction nodes.
- Whether AWS moves the read-only boundary out of the regex filter and into a required database role check.
- Whether teams running self-managed Postgres MCP servers downgrade their connection role to a minimal, non-superuser account.