CVE-2026-61788: @bytebase/dbhub's read-only mode does not prevent database writes

Published Sep 24, 2026
·
Updated

Summary Setting readonly = true on the executesql tool does not make the connection read-only. The connectors are written to set PostgreSQL defaulttransactionreadonly=on (and open SQLite in readOnly mode), but that code is gated on a config value that is never populated, so it never runs. The only thing left enforcing read-only is a classifier that inspects the first keyword of each statement. Any SELECT that writes or has side effects through a function call passes it. With an ordinary role this allows sequence tampering; with a privileged role it allows writing arbitrary files on the server (loexport), reading arbitrary host files (pgreadfile), and remote code execution (dblink + COPY ... TO PROGRAM). The HTTP transport is unauthenticated and binds to 0.0.0.0 by default, so this is reachable by any network caller of /mcp. Details Two problems combine. 1. The database-level read-only control is dead code. PostgresConnector.connect() only enables it when config.readonly is truthy (src/connectors/postgres/index.ts:175-177): ts // SDK-level readonly enforcement: Set defaulttransactionreadonly for the entire connection if (config?.readonly) { poolConfig.options = (poolConfig.options || '') + ' -c defaulttransactionreadonly=on'; } SQLite is gated the same way (src/connectors/sqlite/index.ts:192). ConnectorConfig.readonly is assigned in exactly one place, and only from source.readonly (src/connectors/manager.ts:236-238): ts // Pass readonly flag for SDK-level enforcement (PostgreSQL, SQLite) if (source.readonly !== undefined) { config.readonly = source.readonly; } source.readonly can never have a value: - SourceConfig has no readonly field (src/types/config.ts:49-62). readonly exists only on the per-tool ExecuteSqlToolConfig / CustomToolConfig. - The TOML loader rejects readonly at source level (src/config/toml-loader.ts:476-481: "readonly must be configured per-tool, not per-source"). - The --readonly CLI flag was removed and now hard-exits (src/config/env.ts:30). So the if (source.readonly !== undefined) check is always false, config.readonly stays unset, and DB-level read-only is never applied in any configuration the loader accepts. The per-tool readonly only ever reaches the classifier; executeSQL() ignores options.readonly and runs multi-statement batches in a plain BEGIN rather than BEGIN READ ONLY (src/connectors/postgres/index.ts:598-666). (The docs already describe the classifier as "a safety net... not a security boundary." This report is about the DB-level control above, which the code clearly means to apply — see the "SDK-level readonly enforcement" comments — but silently fails to wire up.) 2. The classifier only checks the leading keyword. areAllStatementsReadOnly() (src/tools/execute-sql.ts:24-27) splits on ; and runs isReadOnlySQL() (src/utils/allowed-keywords.ts) on each statement. isReadOnlySQL matches the first word against an allow-list, scans for mutating keywords only inside WITH, blocks SELECT ... INTO, and special-cases EXPLAIN ANALYZE. It never looks at the functions a statement calls. These all classify as read-only: - SELECT setval('seq', n) / nextval('seq') — sequence write. Needs UPDATE (setval) or USAGE/UPDATE (nextval) on the sequence, which read roles normally hold. - SELECT loexport(lo, '/path') — writes a file on the server. Needs superuser or pgwriteserverfiles. - SELECT pgreadfile('/etc/passwd') — reads any file the server user can read. Needs superuser or pgreadserverfiles. - SELECT dblinkexec('dbname=...', 'UPDATE ...') — opens a fresh connection (not read-only) and runs writes/DDL. Needs the dblink extension. - SELECT dblinkexec('dbname=...', $$COPY (SELECT 1) TO PROGRAM 'id'$$) — command execution. Needs superuser or pgexecuteserverprogram, plus dblink. The read-only test suite covers none of these. PoC Point DBHub at a PostgreSQL source with read-only set on the tool: toml [[sources]] id = "default" dsn = "postgres://app:app@localhost:5432/app" [[tools]] name = "executesql" source = "default" readonly = true Start it and call executesql: npx @bytebase/dbhub@latest --transport http --port 8080 With any role, a write that should be blocked goes through — the sequence value changes and the call returns success: sql SELECT setval('usersidseq', 1); With a privileged role, the rest are also accepted and executed: sql SELECT loexport(lofrombytea(0, decode('48656c6c6f0a','hex')), '/tmp/dbhubpoc'); -- writes /tmp/dbhubpoc SELECT pgreadfile('/etc/passwd'); -- reads a host file SELECT dblinkexec('dbname=app', 'UPDATE users SET admin=true'); -- write via a new connection SELECT dblinkexec('dbname=app', $$COPY (SELECT 1) TO PROGRAM 'id > /tmp/pwned'$$); -- runs a shell command The decision can be reproduced without a database by running the project's own isReadOnlySQL + splitSQLStatements (with areAllStatementsReadOnly copied from src/tools/execute-sql.ts) over the strings above: direct INSERT/UPDATE/DROP, data-modifying CTEs, SELECT ... INTO, and EXPLAIN ANALYZE INSERT are all rejected, while every function-based statement above returns read-only = true. Impact Affects all released versions up to and including 0.22.2, on both stdio and HTTP transports, for PostgreSQL and SQLite. readonly = true does not stop writes. Anyone who can reach the executesql input can modify data under read-only mode — a network caller of the unauthenticated /mcp endpoint, a malicious MCP client, or untrusted content reaching an agent wired to DBHub through prompt injection. When the configured database role is privileged (common, since DBHub is often pointed at an existing admin DSN), the same access yields arbitrary file write on the server, arbitrary host-file read, and remote code execution on the database host.

---

Maintainer note (consolidation)

Tracking this as the canonical advisory for "read-only mode does not prevent database writes." The following reports describe the same root cause (read-only enforced only by the keyword classifier; the connection-level backstop was never wired) and are closed as duplicates:

- GHSA-7rgf-cwgq-c2qc — same unwired driver-level backstop, plus the SQLite write-effecting PRAGMA gap. - GHSA-m689-287g-5xpc — SQLite assignment-form PRAGMA write bypass (a subset of the above).

Preserving the SQLite-specific remediation from those reports: in isReadOnlySQL, the assignment form PRAGMA x = ... must be classified as a write (only the query/introspection form is read-only), and SQLite read-only executions are additionally guarded at the engine via PRAGMA queryonly=ON.

GHSA-j656-3hf2-fvjc (MySQL/MariaDB -- comment parsing + multipleStatements) is a distinct root cause and is tracked separately.

Fix: https://github.com/bytebase/dbhub/pull/342 — adds engine-level read-only enforcement per tool (Postgres BEGIN READ ONLY, SQLite queryonly, MySQL/MariaDB START TRANSACTION READ ONLY) plus the classifier hardening above.

Other sources

DBHub is a database MCP server for Postgres, MySQL, SQL Server, Oracle, MariaDB, SQLite. Prior to version 0.22.6, setting readonly = true on the executesql tool does not make the connection read-only. The connectors are written to set PostgreSQL defaulttransactionreadonly=on (and open SQLite in readOnly mode), but that code is gated on a config value that is never populated, so it never runs. The only thing left enforcing read-only is a classifier that inspects the first keyword of each statement. Any SELECT that writes or has side effects through a function call passes it. With an ordinary role this allows sequence tampering; with a privileged role it allows writing arbitrary files on the server (loexport), reading arbitrary host files (pgreadfile), and remote code execution (dblink + COPY ... TO PROGRAM). The HTTP transport is unauthenticated and binds to 0.0.0.0 by default, so this is reachable by any network caller of /mcp. Version 0.22.6 patches the issue.

— MITRE

Affected Software

2 affected componentsFixes available
npm/@bytebase/dbhub<0.22.6
npm/@bytebase/dbhub<0.22.6
0.22.6

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/@bytebase/dbhub to a version that resolves this vulnerability.

    Fixed in 0.22.6
  2. Upgrade

    Upgrade @bytebase/dbhub to a version that resolves this vulnerability.

    Fixed in 0.22.6

Event History

Sep 24, 2026
CVE Published
via MITRE·05:37 PM
Data Sourced
via MITRE·05:37 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·06:17 PM
DescriptionSeverityWeakness
Advisory Published
via GitHub·07:37 PM
Data Sourced
via GitHub·07:37 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are most exposed?

Instances using the HTTP transport are exposed to network callers because it is unauthenticated and binds to 0.0.0.0 by default. Any caller that can reach the /mcp endpoint can invoke the affected tool.

2

What does an attacker need to bypass the intended restriction?

No database role or prior authentication to the HTTP transport is required to reach the endpoint. The bypass uses a statement beginning with SELECT that performs writes or side effects through a function call, which passes the first-keyword classifier.

3

How does database privilege level affect impact?

With an ordinary database role, an attacker can tamper with sequences. With a privileged PostgreSQL role, they may write server files with lo_export, read host files with pg_read_file, or achieve remote code execution using dblink with COPY ... TO PROGRAM.

4

What version fixes the issue?

Version 0.22.6 patches the issue. Versions prior to 0.22.6 should not rely on the execute_sql readonly setting to enforce a read-only database connection.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203