Skip to content

Build1 publisher2 min readPublished

COMMIT; DROP TABLE slips past the reference Postgres MCP server's read-only guard

Model Context Protocol's reference Postgres MCP server guards writes with a read-only transaction. One COMMIT; DROP TABLE query drops the table anyway. Only database permissions sit where the agent's SQL cannot reach.

The Engineer · Build desk

Illustration accompanying COMMIT; DROP TABLE slips past the reference Postgres MCP server's read-only guard

What happened

  • The archived handler runs each request as three separate calls, opening the transaction, passing the agent's text verbatim, then issuing a ROLLBACK.
  • Because the text is sent with no parameters bound, node-postgres uses the simple query protocol, which accepts several statements in one string, so two commands arrive as a single query.
  • Logged in instead as a role granted pg_read_all_data that owns no tables, the same COMMIT; DROP TABLE failed with 'must be owner of table customers' and the table stayed.
  • Datadog Security Labs documented the identical escape in a case study titled 'SQL injection in the Postgres MCP server' on August 21, 2025.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure Whatever a read tool returns joins the agent's context, so against a cloud-hosted model the customer emails and card digits a SELECT pulls back are now held by the model provider too.
  • constraint A statement_timeout set on the role is only a default; SET LOCAL statement_timeout = 0 overrides it, so a hard limit has to live in the MCP server or the connection pooler, out of the SQL's reach.
  • decision An agent that only needs structure can run as a role with CONNECT, USAGE and no table grants, which reads the whole schema from pg_catalog and is refused every row.

A transaction mode belongs to the session, and the agent's SQL controls the session. Postgres, meanwhile, checks privileges on every statement, whatever transaction it runs in [12]. The agent's query can flip a read-only transaction. It cannot change the grants on a login role.

An honest write still fails inside it: INSERT INTO customers came back with "cannot execute INSERT in a read-only transaction" [6]. The mode catches the write that follows its rules. It misses the one that commits the transaction first.

The tests ran against PostgreSQL 18.3 in a throwaway container, with the reference server's handler reproduced on the same node-postgres driver, version 8.23.0, against a customers table holding emails and card digits [7]. The findings are from the blog of Schemity, a desktop ERD tool whose author discloses building it [19].

Fixing the write does not fix the read. SELECT email, card_last4 FROM customers returned every customer's email and card digits inside the read-only transaction [5], and returned them again under the role that owns nothing [15]. A SELECT-only role stops the DROP but still returns every row the SELECT asked for [17].

Other Postgres MCP servers are separate code, so the escape depends on how yours hands the SQL to the driver: a prepared statement accepts only one statement, a raw string passed through as it arrives accepts several [10]. The reference repository was archived on May 29, 2025, and its README now reads "No security updates or bug fixes will be provided" for these servers [9].

One grant most people forget: COMMIT; CREATE TEMP TABLE t (x int) still succeeds, because PUBLIC may create temporary tables by default. It touches none of your data, and you can revoke TEMP if it bothers you [18].

What to watch

  • Whether Postgres MCP servers in wide use pass the agent's SQL as prepared statements or as raw strings.
  • Whether server authors move the statement timeout into the MCP layer or a pooler instead of the role.
  • Whether the Model Context Protocol project replaces the archived example after Datadog's August report.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories