pg-readonly-mcp
pg-readonly-mcp
Ein schreibgeschützter Postgres-MCP-Server, der SQL parst anstatt es mit Mustererkennung zu behandeln – denn die Alternative ist bereits einmal öffentlich gescheitert, und zwar auf eine Weise, die es wert ist, konkret benannt zu werden.
Die Umgehung, die dieses Tool schließen soll
Der Referenz-Postgres-MCP-Server von Anthropic erzwingt „schreibgeschützt", indem er jede Abfrage in eine schreibgeschützte Transaktion verpackt. Er akzeptierte auch durch Semikolon getrennte Mehrfachanweisungen. Diese Kombination ist ausnutzbar:
SELECT 1; COMMIT; DROP SCHEMA public CASCADE;Das COMMIT beendet die schreibgeschützte Transaktion vorzeitig. Alles danach läuft mit voller Sitzungsberechtigung. Datadog Security Labs hat dies 2026 veröffentlicht; der Server wurde als veraltet markiert und archiviert – und das anfällige Paket hatte danach immer noch 21.000 wöchentliche Downloads.
Eine schreibgeschützte Transaktion ist eine Eigenschaft davon, wie eine Abfrage ausgeführt wird. Sie sagt nichts darüber aus, was die Abfrage ist, genau deshalb war es möglich, sich daraus herauszureden. Dieser Server prüft stattdessen die zweite Sache.
Related MCP server: postgres-mcp-readonly
Zwei unabhängige Ebenen
Jede einzelne hätte die offengelegte Umgehung gestoppt. Beide existieren, weil ein Fehler in einer nicht das Einzige sein sollte, was zwischen einem Agenten und einem Schreibzugriff steht.
1. Das SQL wird geparst, nicht gescannt. guard.py verwendet sqlglot, um einen echten Syntaxbaum zu erstellen, und wendet drei Prüfungen an:
Genau eine Anweisung.
sqlglot.parseteilt an anweisungsbeendenden Semikolons auf, wie es ein Treiber tun würde, sodass das obige Payload zu drei Anweisungen wird und abgelehnt wird, bevor eine davon eine Verbindung erreicht.Die äußere Anweisung ist eine Leseform –
SELECT,UNION,INTERSECT,EXCEPToder ein CTE, das aus diesen aufgebaut ist.DROP TABLE userswird hier abgelehnt.Kein Schreibzugriff irgendwo im Baum, vollständig durchlaufen. Dies ist die Prüfung, die die anderen beiden nicht ersetzen können: Postgres erlaubt es einem CTE, eine datenmodifizierende Anweisung zu tragen, also ist
WITH x AS (DELETE FROM t RETURNING *) SELECT * FROM xauf der äußeren Ebene einSELECT. Eine Prüfung, die nur die äußere Form betrachtet, übersieht dies vollständig. Das Durchlaufen jedes Knotens findet dasDELETEunabhängig davon, wie viele Ebenen darunter es sitzt.
Eine nicht erkannte Anweisungsform – alles, für das sqlglot keine spezifische Regel hat – wird durch dieselbe Regel wie alles andere abgelehnt. Unbekannt ist nicht dasselbe wie sicher.
2. Die Verbindung selbst kann nicht schreiben, unabhängig davon, was sie ausführen soll. check_connection_is_readonly weigert sich zu starten, wenn die verbundene Rolle ein Superuser ist, Datenbanken oder Rollen erstellen kann, die zeilenbasierte Sicherheit umgehen kann oder irgendeine Berechtigung über SELECT hinaus für eine Tabelle besitzt, die sie sehen kann. Nicht erschöpfend – Postgres-Berechtigungen können auch durch Besitz, PUBLIC-Grants oder RLS-Richtlinien kommen, die diese Prüfung nicht aufzählt – aber es erfasst die beiden Arten, wie eine Fehlkonfiguration am häufigsten auftritt, hier explizit genannt statt impliziert.
Wovor es nicht schützt
Ein privilegierter Verbindungsstring, der ihm trotzdem übergeben wird. Die Startprüfung erfasst die üblichen Formen von Überprivilegierung; es ist keine erschöpfende Berechtigungsprüfung, und es sagt dies oben explizit, statt es zu implizieren.
Ressourcenerschöpfung innerhalb der Grenzen. Eine Abfrage, die legal 1.000 Zeilen sehr breiter Daten zurückgibt oder die legitimerweise teuer zu planen ist, kostet trotzdem, was sie kostet. Zeilenbegrenzungen und ein Statement-Timeout begrenzen den Schaden; sie machen eine teure Leseoperation nicht kostenlos.
Was der Agent mit den Daten macht, sobald er sie hat. Dies ist ein Abfrage-Gate, kein Tool zur Verhinderung von Datenverlust. Lesezugriff auf eine Tabelle ist Lesezugriff auf das, was darin ist.
Eine Parser-Diskrepanz.
sqlglotund Postgres' eigener Parser sind zwei unabhängige Implementierungen derselben Grammatik. Es ist nicht bewiesen, dass sie sich in jedem Randfall einig sind, den Postgres akzeptiert – eine echte, wenn auch schmale Lücke in einem Projekt, dessen ganze Prämisse darin besteht, keiner einzelnen Ebene zu vertrauen. Ebene zwei existiert teilweise deswegen: selbst wenn eine Parser-Differenz etwas Unbeabsichtigtes durch die Wächter lässt, kann die darunterliegende Verbindung trotzdem nicht schreiben.
Scanner-Ergebnisse
Ausgeführt gegen agent-audit 0.19.2 am 18. August 2026: 15 Fundstellen, 11 automatisch unterdrückt, 4 handlungsrelevant – 1 BLOCK, 3 WARN.
Der BLOCK-Fund ist der interessanteste und es lohnt sich, ihn vollständig zu lesen. server.py:156, Konfidenz 1.0: cur.execute(sql) – als SQL-Injection durch unparametrisierte Ausführung gekennzeichnet. Diese Zeile ist echt. Sie ist auch die am stärksten verteidigte Zeile in dieser Codebasis: Wenn sql sie erreicht, hat validate_readonly() es bereits geparst, bestätigt, dass es genau eine Anweisung ist, bestätigt, dass diese Anweisung eine Leseform ist, und jeden Knoten darin auf Schreibzugriff überprüft. Der Scanner hat keine Möglichkeit, all das zu sehen – es ist ein einzeiliger Mustervergleich, und die Validierung erfolgt in einer anderen Funktion, in einem anderen Modul, mehrere Zeilen früher. Es identifiziert korrekt die Form, die allgemein gefährlich ist, und kann nicht sehen, dass die Form bereits überprüft wurde.
Es hätte auch nicht durch das befriedigt werden können, was es vorschlägt. Parametrisierte Abfragen schützen Werte, die in eine feste Abfrageform eingesetzt werden – WHERE id = %s. Sie treffen hier nicht zu, weil die Struktur der Abfrage die Eingabe ist, die dieses Tool akzeptieren soll. Es gibt kein reines Werte-Parametrisierungsschema für „führe das schreibgeschützte SQL aus, das der Aufrufer verlangt." Die Lösung für diese Art von Tool ist die Validierung der Struktur, wofür der Rest dieser Datei da ist.
Die WARN zur Definition von query() (AGENT-034, „keine Eingabevalidierung im Funktionskörper") ist derselbe blinde Fleck aus einem anderen Blickwinkel: Die erste Zeile der Funktion ruft validate_readonly(sql) innerhalb eines try/except auf. Ein Aufruf einer importierten Funktion ist kein Muster, das der Scanner als Validierung anerkennt.
Die beiden WARNs zu hartcodierten Anmeldeinformationen befinden sich in tests/conftest.py: der Standard-Admin-DSN (postgres:postgres@localhost:5432/postgres, der Standard für lokale/CI-Postgres) und das wörtliche Passwort, das für Rollen verwendet wird, die die Testsuite innerhalb desselben Tests erstellt und löscht. Beide werden korrekt als anmeldeinformationsförmige Zeichenketten identifiziert; keine davon ist eine Anmeldeinformation, die irgendetwas schützt – eine zeigt auf eine Wegwerf-Lokaldatenbank, die andere lebt für die Dauer eines einzigen Tests.
Die verbleibenden 11 Fundstellen – alle AGENT-041, alle in Test-Fixtures, die CREATE SCHEMA / GRANT / DROP ROLE-Anweisungen aus von uuid.uuid4() abgeleiteten Namen erstellen – wurden bereits vom Scanner selbst automatisch unterdrückt.
Ein Musterscanner ist ein Rauchmelder, kein Richter. Zu veröffentlichen, was er findet und warum, ist mehr wert als eine saubere Zahl für sich allein.
Installation
pip install -e .Konfiguration
Erfordert einen Verbindungsstring, entweder als --dsn oder über PG_READONLY_MCP_DSN – die Umgebungsvariable existiert, damit ein Passwort nicht in einer Befehlszeile oder in einer Client-Konfigurationsdatei erscheinen muss, die versehentlich committet werden könnte:
{
"mcpServers": {
"pg-readonly-mcp": {
"command": "pg-readonly-mcp",
"env": { "PG_READONLY_MCP_DSN": "postgresql://readonly_role:...@host:5432/db" }
}
}
}Die Rolle in diesem Verbindungsstring darf nichts über SELECT hinaus besitzen. Der Server prüft dies selbst und weigert sich sonst zu starten – siehe check_connection_is_readonly oben.
Das Tool
Ein Tool, bewusst. Ein Server, dessen gesamtes Wertversprechen „wir lehnen alles außer Lesevorgängen ab" ist, braucht keine zweite Oberfläche, um das auch richtig zu machen.
Tool | Was es tut |
| Führt eine SELECT-förmige Anweisung aus. Geparst und durchlaufen, bevor sie die Verbindung berührt. Begrenzt auf |
Tests
37 Tests. 25 benötigen keine Datenbank und laufen überall – sie sind die gesamte Testsuite von guard.py, reiner String rein, Entscheidung raus. Die anderen 12 benötigen eine echte Postgres-Instanz und sind bewusst Integrationstests: Der Sinn von check_connection_is_readonly ist, was es gegen echte Rollenattribute und echte Grants tut, und eine gemockte Verbindung würde bestehen, unabhängig davon, was der Server tatsächlich gegen eine echte tut.
pytest -q --cov=pg_readonly_mcpCI läuft gegen einen echten postgres:16-Servicecontainer und schlägt den Build fehl, wenn die datenbankgestützten Tests dort als übersprungen gemeldet werden – dieselbe Regel, die sweep-mcp auf seine Symlink-Tests anwendet, aus demselben Grund: Ein Test, der stillschweigend nichts tut, ist schlimmer als kein Test.
Unter den 12: eine Live-Reproduktion des exakten Datadog-Payloads, gesteuert durch den tatsächlichen MCP-Toolaufruf statt direkt durch guard.py – und eine Behauptung, dass das Zielschema danach noch existiert, nicht nur, dass eine Ausnahme ausgelöst wurde. Ebenfalls abgedeckt: ein Schreibzugriff, der in einem CTE versteckt ist, über denselben Aufrufpfad, die Zeilenbegrenzung, das Statement-Timeout, dass eine abgebrochene Abfrage die Verbindung für die nächste nutzbar lässt, und dass eine Rolle mit CREATEDB und null Tabellen-Grants allein aufgrund des Rollenattributs abgelehnt wird.
Abdeckung auf CI, gegen eine echte Postgres-Instanz: 78%, guard.py bei 100%. Die Lücke in server.py ist der generische psycopg.Error-Catch-all – nichts in der Suite provoziert absichtlich einen Datenbankfehler, der keine Abbrechung ist – und die argparse- und Transportverdrahtung von main(), die die Suite stattdessen direkt über build_server übt; der Transport ist der Teil, der am wenigsten mockenswert ist und am unwahrscheinlichsten der Ort ist, an dem sich ein echter Fehler versteckt.
Die erste Version, die gepusht wurde, bestand nicht. Zwei Fehler traten erst auf, als ein echter Postgres-Servicecontainer die Suite zum ersten Mal ausführte, und keiner war allein aus dem Code ersichtlich: SET statement_timeout = %s erreichte Postgres als SET statement_timeout = $1 und konnte nicht geparst werden, weil SET eine Utility-Anweisung ist und keinen Bind-Parameter akzeptiert, wie es ein SELECT tut – jeder echte Aufruf des Tools wäre identisch fehlgeschlagen. Separat versuchten die Test-Fixtures, eine Rolle mit DROP ROLE zu löschen, die noch aktive Grants besaß, was Postgres ablehnt; DROP OWNED BY muss zuerst ausgeführt werden. Beide sind behoben, und der Lauf seitdem ist der, den diese Zahlen beschreiben. Behalten, weil eine Suite, die nur Erfolge meldet, eine Suite ist, die niemand hat scheitern sehen.
Aufbau
src/pg_readonly_mcp/
guard.py parses and walks the tree. Opens no connection. 135 lines.
server.py the MCP tool, the connection check, the timeout and row cap.
tests/
test_guard.py 25 tests - no database, run anywhere
test_server.py 12 tests - live Postgres required, CI-enforcedguard.py weiß nichts über MCP oder psycopg. Wenn eine Abfrage jemals aus einem Grund abgelehnt wird, der stattdessen in server.py liegt, ist das ein Fehler – das Urteil gehört eine Ebene tiefer, wo es mit einem String und nichts anderem getestet werden kann.
Lizenz
MIT.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityCmaintenanceRead-only PostgreSQL MCP server that enables running SELECT queries, listing tables and schemas, and describing columns, with built-in protection against writes and malicious SQL attacks.539MIT
- AlicenseAqualityDmaintenanceA secure, read-only PostgreSQL MCP server that provides safe database introspection and querying capabilities.1415MIT
- Flicense-qualityBmaintenanceRead-only MCP server for PostgreSQL enabling schema introspection and SELECT queries via MCP clients like Claude, with multi-layered write protection.
- Alicense-qualityCmaintenanceProvides a read-only PostgreSQL MCP server with schema introspection. Enforces least-privilege database roles to prevent any writes, even from malicious SQL.MIT
Related MCP Connectors
MCP server for managing Prisma Postgres.
Read-only MCP server for ClassQuill, a tutoring-business-management platform.
MCP server for interacting with the Supabase platform
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/les-k/pg-readonly-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server