Skip to main content
Glama
les-k

pg-readonly-mcp

by les-k

pg-readonly-mcp

읽기 전용 Postgres MCP 서버로, 패턴 매칭 대신 SQL을 파싱합니다. 대안이 이미 공개적으로 실패했고, 그 실패에 대해 구체적으로 언급할 가치가 있기 때문입니다.

이 서버가 막고자 하는 우회 방법

Anthropic의 참조 Postgres MCP 서버는 각 쿼리를 읽기 전용 트랜잭션으로 감싸서 "읽기 전용"을 강제했습니다. 또한 세미콜론으로 구분된 다중 명령문 입력을 허용했습니다. 이 조합은 악용 가능합니다:

SELECT 1; COMMIT; DROP SCHEMA public CASCADE;

COMMIT은 읽기 전용 트랜잭션을 조기에 종료시킵니다. 그 이후의 모든 것은 전체 세션 권한으로 실행됩니다. Datadog Security Labs가 2026년에 이를 공개했습니다; 해당 서버는 더 이상 사용되지 않고 보관 처리되었습니다. 하지만 취약한 패키지는 그 후에도 주간 21,000회 다운로드를 기록하고 있었습니다.

읽기 전용 트랜잭션은 쿼리가 어떻게 실행되는지에 대한 속성입니다. 쿼리가 무엇인지에 대해서는 아무것도 말하지 않으며, 이것이 바로 우회가 가능했던 이유입니다. 이 서버는 두 번째 것을 대신 확인합니다.

Related MCP server: postgres-mcp-readonly

두 개의 독립적인 계층

둘 중 하나만 있어도 공개된 우회 방법을 막을 수 있었을 것입니다. 둘 다 존재하는 이유는 한 계층의 결함이 에이전트와 쓰기 작업 사이의 유일한 장벽이 되어서는 안 되기 때문입니다.

1. SQL이 스캔되지 않고 파싱됩니다. guard.pysqlglot을 사용하여 실제 구문 트리를 구축하고 세 가지 검사를 적용합니다:

  • 정확히 하나의 명령문. sqlglot.parse는 드라이버가 하는 것처럼 명령문 종료 세미콜론에서 분할하므로, 위의 페이로드는 세 개의 명령문이 되어 그중 어떤 것도 연결에 도달하기 전에 거부됩니다.

  • 외부 명령문은 읽기 형태입니다 — SELECT, UNION, INTERSECT, EXCEPT, 또는 이들로 구성된 CTE. DROP TABLE users는 여기서 거부됩니다.

  • 트리 전체에서 쓰기가 없음을 완전히 탐색합니다. 이것은 다른 두 검사가 대체할 수 없는 검사입니다: Postgres는 CTE가 데이터 수정 명령문을 포함할 수 있으므로, WITH x AS (DELETE FROM t RETURNING *) SELECT * FROM x는 외부 수준에서 SELECT입니다. 외부 형태만 확인하는 검사는 이것을 완전히 놓칩니다. 모든 노드를 탐색하면 DELETE가 몇 수준 아래에 있든 관계없이 찾아냅니다.

인식되지 않은 명령문 형태 — sqlglot에 특정 규칙이 없는 모든 것 — 는 다른 모든 것과 동일한 규칙으로 거부됩니다. 익숙하지 않은 것이 안전한 것과 같지는 않습니다.

2. 연결 자체가 실행 요청과 무관하게 쓸 수 없습니다. check_connection_is_readonly는 연결된 역할이 슈퍼유저이거나, 데이터베이스나 역할을 생성할 수 있거나, 행 수준 보안을 우회할 수 있거나, 볼 수 있는 모든 테이블에 대해 SELECT를 초과하는 권한을 보유한 경우 시작을 거부합니다. 완전하지는 않습니다 — Postgres 권한은 소유권, PUBLIC 권한 부여, 또는 이 검사가 열거하지 않는 RLS 정책을 통해서도 올 수 있습니다 — 하지만 잘못된 구성이 가장 일반적으로 나타나는 두 가지 방식을 포착하며, 암시적으로 말하지 않고 명시적으로 설명합니다.

보호하지 않는 것

  • 어쨌든 전달된 권한 있는 연결 문자열. 시작 검사는 과도한 권한의 일반적인 형태를 포착합니다. 완전한 권한 감사는 아니며, 위에서 암시하지 않고 명시적으로 설명합니다.

  • 한계 내에서의 리소스 고갈. 합법적으로 1,000행의 매우 넓은 데이터를 반환하는 쿼리, 또는 합법적으로 계획하는 데 비용이 많이 드는 쿼리는 여전히 그 비용만큼 듭니다. 행 제한과 명령문 시간 제한이 피해를 제한합니다. 비용이 많이 드는 읽기를 무료로 만들지는 않습니다.

  • 에이전트가 데이터를 얻은 후에 하는 일. 이것은 쿼리 게이트이지 데이터 손실 방지 도구가 아닙니다. 테이블에 대한 읽기 액세스는 그 안에 있는 모든 것에 대한 읽기 액세스입니다.

  • 파서 불일치. sqlglot과 Postgres 자체 파서는 동일한 문법의 두 독립적인 구현입니다. Postgres가 받아들이는 모든 경계 사례에 대해 일치한다는 것이 입증되지 않았습니다 — 단일 계층을 신뢰하지 않는다는 전체 전제를 가진 프로젝트에서 진정하지만 좁은 격차입니다. 두 번째 계층이 부분적으로 존재하는 이유입니다: 파서 차이로 인해 의도하지 않은 것이 가드를 통과하더라도, 그 아래의 연결은 여전히 쓸 수 없습니다.

스캐너 결과

2026년 8월 18일 agent-audit 0.19.2에 대해 실행: 15개의 발견, 11개 자동 억제, 4개 조치 가능 — 1 BLOCK, 3 WARN.

BLOCK 발견이 가장 흥미롭고, 전문을 읽을 가치가 있습니다. server.py:156, 신뢰도 1.0: cur.execute(sql) — 매개변수화되지 않은 실행을 통한 SQL 인젝션으로 플래그 지정됨. 해당 라인은 실제입니다. 또한 이 코드베이스에서 가장 많이 방어된 라인이기도 합니다: sql이 해당 라인에 도달할 때쯤이면 validate_readonly()가 이미 이를 파싱하고, 정확히 하나의 명령문임을 확인하고, 해당 명령문이 읽기 형태임을 확인하고, 쓰기를 확인하기 위해 모든 노드를 탐색했습니다. 스캐너는 이 중 어떤 것도 볼 방법이 없습니다 — 단일 파일 패턴 매치이며, 유효성 검사는 다른 함수, 다른 모듈, 여러 라인 앞에서 발생합니다. 일반적으로 위험한 형태를 올바르게 식별하지만, 그 형태가 이미 확인되었음을 볼 수 없습니다.

또한 제안하는 대로 수행했다면 만족될 수 없었을 것입니다. 매개변수화된 쿼리는 고정된 쿼리 형태에 대입된 을 보호합니다 — WHERE id = %s. 여기에는 적용되지 않습니다. 왜냐하면 쿼리의 구조가 이 도구가 받아들이기 위해 존재하는 입력이기 때문입니다. "호출자가 요청하는 모든 읽기 전용 SQL을 실행"하는 것에 대한 값 전용 매개변수화 체계는 없습니다. 이 클래스의 도구에 대한 수정은 구조를 검증하는 것이며, 이것이 이 파일의 나머지 부분이 하는 일입니다.

query() 정의에 대한 WARN (AGENT-034, "함수 본문에 입력 검증 없음")은 다른 각도에서 본 동일한 사각지대입니다: 함수의 첫 번째 줄은 try/except 내에서 validate_readonly(sql)을 호출합니다. 가져온 함수에 대한 호출은 스캐너가 검증으로 인정하는 패턴이 아닙니다.

하드코딩된 자격 증명에 대한 두 개의 WARNtests/conftest.py에 있습니다: 기본 관리자 DSN (postgres:postgres@localhost:5432/postgres, 표준 로컬/CI Postgres 기본값)과 테스트 스위트가 동일한 테스트 내에서 생성하고 삭제하는 역할에 사용된 리터럴 비밀번호입니다. 둘 다 자격 증명 형태의 문자열로 올바르게 식별됩니다. 둘 다 아무것도 보호하지 않는 자격 증명입니다 — 하나는 일회용 로컬 데이터베이스를 가리키고, 다른 하나는 단일 테스트 기간 동안만 존재합니다.

나머지 11개의 발견 — 모두 AGENT-041, 모두 uuid.uuid4()에서 파생된 이름으로 CREATE SCHEMA / GRANT / DROP ROLE 명령문을 구축하는 테스트 픽스처에 있음 — 은 이미 스캐너 자체에 의해 자동 억제되었습니다.

패턴 스캐너는 연기 감지기이지 판사가 아닙니다. 발견한 내용과 그 이유를 공개하는 것이 깔끔한 숫자만 있는 것보다 더 가치 있습니다.

설치

pip install -e .

구성

연결 문자열이 필요하며, --dsn으로 제공되거나 PG_READONLY_MCP_DSN을 통해 제공됩니다 — 환경 변수는 비밀번호가 명령줄이나 실수로 커밋될 수 있는 클라이언트 구성 파일에 나타나지 않도록 존재합니다:

{
  "mcpServers": {
    "pg-readonly-mcp": {
      "command": "pg-readonly-mcp",
      "env": { "PG_READONLY_MCP_DSN": "postgresql://readonly_role:...@host:5432/db" }
    }
  }
}

해당 연결 문자열의 역할은 SELECT 외에는 아무것도 보유하지 않아야 합니다. 서버는 이를 자체적으로 확인하고 그렇지 않으면 시작을 거부합니다 — 위의 check_connection_is_readonly를 참조하세요.

도구

의도적으로 하나의 도구입니다. 전체 가치 제안이 "읽기를 제외한 모든 것을 거부합니다"인 서버는 이를 올바르게 처리하기 위해 두 번째 표면이 필요하지 않습니다.

도구

기능

query

하나의 SELECT 형태 명령문을 실행합니다. 연결에 닿기 전에 파싱되고 탐색됩니다. --max-rows(기본값 1000) 및 --timeout-ms(기본값 5000)으로 제한됩니다.

테스트

37개의 테스트. 25개는 데이터베이스가 필요 없으며 어디서나 실행됩니다 — 이들은 guard.py의 전체 테스트 스위트로, 순수 문자열 입력, 결정 출력입니다. 나머지 12개는 라이브 Postgres가 필요하며 설계상 통합 테스트입니다: check_connection_is_readonly의 요점은 실제 역할 속성과 실제 권한 부여에 대해 수행하는 작업이며, 모의 연결은 서버가 실제 연결에 대해 실제로 수행하는 작업과 관계없이 통과할 것입니다.

pytest -q --cov=pg_readonly_mcp

CI는 실제 postgres:16 서비스 컨테이너에 대해 실행되며, 데이터베이스 기반 테스트가 건너뛰기로 보고되면 빌드를 실패 처리합니다sweep-mcp가 심볼릭 링크 테스트에 적용하는 것과 동일한 규칙이며, 같은 이유입니다: 조용히 아무것도 하지 않는 테스트는 테스트가 없는 것보다 나쁩니다.

12개 중: guard.py를 통하지 않고 실제 MCP 도구 호출을 통해 구동되는 정확한 Datadog 페이로드의 라이브 재현 — 그리고 예외가 발생했다는 것뿐만 아니라 대상 스키마가 그 후에도 여전히 존재한다는 단언. 또한 포함: 동일한 호출 경로를 통해 CTE 내에 숨겨진 쓰기, 행 제한, 명령문 시간 제한, 취소된 쿼리가 다음 쿼리를 위해 연결을 사용 가능한 상태로 유지하는 것, 그리고 CREATEDB와 테이블 권한이 전혀 없는 역할이 역할 속성만으로 거부되는 것.

실제 Postgres에 대한 CI에서의 커버리지: 78%, guard.py는 100%. server.py의 격차는 일반적인 psycopg.Error 포괄 처리입니다 — 스위트에서 취소가 아닌 데이터베이스 오류를 의도적으로 유발하는 것은 없습니다 — 그리고 main()의 argparse 및 전송 배선으로, 스위트는 대신 build_server를 통해 직접 실행합니다; 전송은 모의할 가치가 가장 적고 실제 실수가 숨을 가능성이 가장 낮은 부분입니다.

처음 푸시된 버전은 통과하지 못했습니다. 실제 Postgres 서비스 컨테이너가 처음으로 스위트를 실행했을 때만 두 개의 버그가 표면화되었으며, 코드만으로는 둘 다 보이지 않았습니다: SET statement_timeout = %sSET statement_timeout = $1로 Postgres에 도달하여 파싱에 실패했습니다. SET은 유틸리티 명령문이고 SELECT처럼 바인드 매개변수를 받지 않기 때문입니다 — 도구에 대한 모든 실제 호출이 동일하게 실패했을 것입니다. 별도로, 테스트 픽스처는 여전히 라이브 권한을 보유한 역할을 DROP ROLE하려고 시도했으며, Postgres는 이를 거부합니다. DROP OWNED BY가 먼저 실행되어야 합니다. 둘 다 수정되었으며, 그 이후의 실행이 이 숫자가 설명하는 실행입니다. 성공만 보고하는 스위트는 아무도 실패를 지켜본 적이 없는 스위트이기 때문에 남겨두었습니다.

레이아웃

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-enforced

guard.py는 MCP나 psycopg에 대해 아무것도 모릅니다. 쿼리가 server.py에 있는 이유로 거부된다면, 그것은 버그입니다 — 판단은 문자열만으로 테스트할 수 있는 한 계층 아래에 속합니다.

라이선스

MIT.

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

  • A
    license
    -
    quality
    C
    maintenance
    Read-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.
    539
    MIT
  • F
    license
    -
    quality
    B
    maintenance
    Read-only MCP server for PostgreSQL enabling schema introspection and SELECT queries via MCP clients like Claude, with multi-layered write protection.
  • A
    license
    -
    quality
    C
    maintenance
    Provides a read-only PostgreSQL MCP server with schema introspection. Enforces least-privilege database roles to prevent any writes, even from malicious SQL.
    MIT

View all related MCP servers

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

View all MCP Connectors

Latest Blog Posts

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