CVE-2026-61788
@bytebase/dbhub's read-only mode does not prevent database writes
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 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. Version 0.22.6 patches the issue.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- High
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- High
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
7.4
@bytebase/dbhub's read-only mode does not prevent database writes
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 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. Version 0.22.6 patches the issue.
- Record
CVE-2026-61788- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-184, CWE-636, CWE-863
- Updated
- 2026-11-02
EUVDEUVD-2026-86326
7.4
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 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. Version 0.22.6 patches the issue.
- Record
EUVD-2026-86326- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-mwwr-p57h-56pf
7.4
@bytebase/dbhub's read-only mode does not prevent database writes
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:
SourceConfighas noreadonlyfield (src/types/config.ts:49-62).readonlyexists only on the per-toolExecuteSqlToolConfig/CustomToolConfig.- The TOML loader rejects
readonlyat source level (src/config/toml-loader.ts:476-481: "readonly must be configured per-tool, not per-source"). - The
--readonlyCLI 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 orpg_write_server_files.SELECT pg_read_file('/etc/passwd')— reads any file the server user can read. Needs superuser orpg_read_server_files.SELECT dblink_exec('dbname=...', 'UPDATE ...')— opens a fresh connection (not read-only) and runs writes/DDL. Needs thedblinkextension.SELECT dblink_exec('dbname=...', $$COPY (SELECT 1) TO PROGRAM 'id'$$)— command execution. Needs superuser orpg_execute_server_program, plusdblink.
The read-only test…
- Record
GHSA-mwwr-p57h-56pf- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-184, CWE-636, CWE-863
- Updated
- 2026-11-02
NVD
7.4
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 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. Version 0.22.6 patches the issue.
- Record
CVE-2026-61788- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-184, CWE-636, CWE-863
- Updated
- 2026-11-02
OSV
7.4
@bytebase/dbhub's read-only mode does not prevent database writes
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 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. Version 0.22.6 patches the issue.
- Record
CVE-2026-61788- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-184, CWE-636, CWE-863
- Updated
- 2026-11-02
OSVGHSA-mwwr-p57h-56pf
7.4
@bytebase/dbhub's read-only mode does not prevent database writes
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:
SourceConfighas noreadonlyfield (src/types/config.ts:49-62).readonlyexists only on the per-toolExecuteSqlToolConfig/CustomToolConfig.- The TOML loader rejects
readonlyat source level (src/config/toml-loader.ts:476-481: "readonly must be configured per-tool, not per-source"). - The
--readonlyCLI 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 orpg_write_server_files.SELECT pg_read_file('/etc/passwd')— reads any file the server user can read. Needs superuser orpg_read_server_files.SELECT dblink_exec('dbname=...', 'UPDATE ...')— opens a fresh connection (not read-only) and runs writes/DDL. Needs thedblinkextension.SELECT dblink_exec('dbname=...', $$COPY (SELECT 1) TO PROGRAM 'id'$$)— command execution. Needs superuser orpg_execute_server_program, plusdblink.
The read-only test…
- Record
GHSA-mwwr-p57h-56pf- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-184, CWE-636, CWE-863
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
@bytebase/dbhubnpm |
before 0.22.6 | 0.22.6 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-61788 is a high vulnerability (CVSS 7.4) in Bytebase Dbhub, published September 24, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.40%, the 34th percentile. Fixed in @bytebase/dbhub 0.22.6.
References
github.com/bytebase/dbhub/security/advisories/GHSA-mwwr-p57h-56pf
nvd.nist.gov/vuln/detail/CVE-2026-61788
github.com/bytebase/dbhub/pull/342
github.com/bytebase/dbhub/commit/872bb338f7d31f6afe517a076ac3e3edafaaaf08
github.com/bytebase/dbhub/releases/tag/v0.22.6
github.com/advisories/GHSA-mwwr-p57h-56pf
github.com/CVEProject/cvelistV5/tree/main/cves/2026/61xxx/CVE-2026-61788.json
github.com/bytebase/dbhub
Left out here: the rest of 2 source descriptions. The JSON record has them.