Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
HIGH severity

CVE-2026-61788 — @bytebase/dbhub

HIGHFix: bytebase/dbhub#342

CVE-2026-61788 is a high-severity (CVSS 7.4) CWE-184 vulnerability in @bytebase/dbhub. A fix is available for @bytebase/dbhub — see the affected versions and patch details below.

@bytebase/dbhub's read-only mode does not prevent database writes

Also known asGHSA-mwwr-p57h-56pf
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 23, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • A successful exploit gives an attacker total control of the affected component, not partial access.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-61788.

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs22th percentile — riskier than 22% of all scored CVEsHighest risk
0.00%0.27%0.54%0.80%0.3%0.3%Oct 26Oct 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

CVE-2026-61788 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 394,803 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

0other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
@bytebase/dbhubnpm
17Kdownloads / week

Description

Summary

Setting readonly = true on the execute_sql tool does not make the connection read-only. The connectors are written to set PostgreSQL default_transaction_read_only=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 (lo_export), reading arbitrary host files (pg_read_file), 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):

// SDK-level readonly enforcement: Set default_transaction_read_only for the entire connection
if (config?.readonly) {
  poolConfig.options = (poolConfig.options || '') + ' -c default_transaction_read_only=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):

// 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 lo_export(lo, '/path') — writes a file on the server. Needs superuser or pg_write_server_files.
  • SELECT pg_read_file('/etc/passwd') — reads any file the server user can read. Needs superuser or pg_read_server_files.
  • SELECT dblink_exec('dbname=...', 'UPDATE ...') — opens a fresh connection (not read-only) and runs writes/DDL. Needs the dblink extension.
  • SELECT dblink_exec('dbname=...', $$COPY (SELECT 1) TO PROGRAM 'id'$$) — command execution. Needs superuser or pg_execute_server_program, 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:

[[sources]]
id = "default"
dsn = "postgres://app:app@localhost:5432/app"
 
[[tools]]
name = "execute_sql"
source = "default"
readonly = true

Start it and call execute_sql:

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:

SELECT setval('users_id_seq', 1);

With a privileged role, the rest are also accepted and executed:

SELECT lo_export(lo_from_bytea(0, decode('48656c6c6f0a','hex')), '/tmp/dbhub_poc');   -- writes /tmp/dbhub_poc
SELECT pg_read_file('/etc/passwd');                                                  -- reads a host file
SELECT dblink_exec('dbname=app', 'UPDATE users SET admin=true');                     -- write via a new connection
SELECT dblink_exec('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 execute_sql 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 query_only=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 query_only, MySQL/MariaDB START TRANSACTION READ ONLY) plus the classifier hardening above.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npm@bytebase/dbhuball versions0.22.6npm install @bytebase/dbhub@0.22.6

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for @bytebase/dbhub, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update @bytebase/dbhub to 0.22.6 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-61788 is resolved across your whole dependency graph.

  3. Workarounds

    Put an independent control in front of the weakness: restrict the affected endpoint or interface to trusted networks, require an additional authentication factor or proxy-level check, and invalidate existing sessions and credentials in case the flaw has already been used.

Frequently Asked Questions

### Summary Setting `readonly = true` on the `execute_sql` tool does not make the connection read-only. The connectors are written to set PostgreSQL `default_transaction_read_only=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
O3 Security · Impact-Aware SCA

Is CVE-2026-61788 in your dependencies?

Find it across npm, including transitive dependencies.

CVE-2026-61788: @bytebase/dbhub RCE — Fixed in 0.22.6