Security batch: cargo-deny gate + lru UAF fix, tenant-session SQL screen, opensrv tls fence - #34
Merged
Merged
Conversation
cargo-deny now runs in CI (deny.toml + a deny job), so a new advisory against the tree fails the build instead of sitting unnoticed. Fixed by version bumps: - lru 0.12.5 -> 0.18.2 -- RUSTSEC-2026-0253 (use-after-free in LruCache::pop; 0.12.5 backs the production TranslateCache) and RUSTSEC-2026-0002 (IterMut unsound). - mysql_async 0.34 -> 0.37 (dev, e2e client) -- drops its own lru 0.12 copy and its crossbeam dependency. - cargo update: anyhow 1.0.104 (RUSTSEC-2026-0190), crossbeam-epoch 0.9.20 (RUSTSEC-2026-0204), postgres-protocol 0.6.12 (RUSTSEC-2026-0179/0180), rustls-webpki 0.103.14. Ignored, each with the reason and the drop condition in deny.toml: - rustls-webpki 0.102.8/0.101.7 CRL and name-constraint advisories -- old copies pinned under opensrv-mysql (server-side TLS only) and tiberius (dev-only TDS test client). - rustls-pemfile 1.x / proc-macro-error2 unmaintained -- transitive, no runtime exposure, awaiting upstream. - time 0.3.45 / tokio-postgres 0.7.17 -- fixed releases require a newer Rust than the workspace MSRV (1.85); drop when the MSRV moves.
A session established through a ConnectionAuthenticator is tenant-scoped: the backend the authenticator returns is the whole world that connection may touch. But every tenant database is opened by one process under one uid, so ATTACH DATABASE '<other tenant file>' -- which passes MySQL-dialect translation verbatim -- reached the rusqlite backend and executed: cross-tenant read/write through an authenticated session. Only the Turso backend refused it, and only because that engine happens to. The MySQL frontend now wraps every authenticator-established session in litewire_backend::tenant_screen, which refuses ATTACH/DETACH, VACUUM with a target (VACUUM INTO), and the path-bearing or schema-reopening PRAGMAs (data_store_directory, temp_store_directory, writable_schema) with a clean statement-level SQL error, on every backend, whatever the engine underneath would do. The screen is quote-, comment-, and multi-statement-aware, sees through EXPLAIN / EXPLAIN QUERY PLAN wrappers and schema-qualified or quoted PRAGMA spellings, and treats malformed SQL (unterminated quote or block comment) conservatively as forbidden. Single-tenant sessions -- a fixed backend, or an embedder driving a BackendConn directly -- are deliberately not screened: ATTACH is legitimate in single-user embedded setups, and the screen keys off the session being tenant-scoped, never off the statement alone. A TenantScreened backend decorator is exported for embedders that want the screen on every route to a per-tenant backend, not only wire sessions. Wire-level regression tests (mysql_async against a two-tenant listener): the ATTACH attempt and each variation come back as SQL errors with the screen's own message, the session survives and still sees only its own data, and a fixed-backend server keeps full ATTACH freedom. The README multi-tenant section now states the guarantee precisely.
…d silently opensrv-mysql calls authenticate() conditionally on the handshake username being present, and its parser yields None on the pre-TLS CLIENT_SSL branch. The tls feature (a default) is what makes that branch unreachable: init_after_ssl re-reads and re-parses the handshake, which produces a username. Dropping the feature -- a natural-looking default-features = false to trim dependencies -- would compile that re-parse out and make the no-username path reachable on the multi-tenant authenticating path, where authenticate() is the only tenant boundary. The handler fails closed structurally regardless (no successful authenticate() means no backend to reach), but the requirement should be enforced, not incidental. Two guards, per issue #32: - The workspace manifest declares features = ["tls"] explicitly, with a SECURITY comment, so disabling default features no longer drops it. - litewire-mysql imports the tls-gated secure_run_with_options re-export under a fence alias, so a build without the feature fails to compile -- litewire otherwise uses only ungated opensrv APIs and would have built silently. Closes #32.
luthermonson
added a commit
that referenced
this pull request
Sep 1, 2026
…een, opensrv tls fence (#34) * build(deps): clear every RustSec advisory and gate CI with cargo-deny cargo-deny now runs in CI (deny.toml + a deny job), so a new advisory against the tree fails the build instead of sitting unnoticed. Fixed by version bumps: - lru 0.12.5 -> 0.18.2 -- RUSTSEC-2026-0253 (use-after-free in LruCache::pop; 0.12.5 backs the production TranslateCache) and RUSTSEC-2026-0002 (IterMut unsound). - mysql_async 0.34 -> 0.37 (dev, e2e client) -- drops its own lru 0.12 copy and its crossbeam dependency. - cargo update: anyhow 1.0.104 (RUSTSEC-2026-0190), crossbeam-epoch 0.9.20 (RUSTSEC-2026-0204), postgres-protocol 0.6.12 (RUSTSEC-2026-0179/0180), rustls-webpki 0.103.14. Ignored, each with the reason and the drop condition in deny.toml: - rustls-webpki 0.102.8/0.101.7 CRL and name-constraint advisories -- old copies pinned under opensrv-mysql (server-side TLS only) and tiberius (dev-only TDS test client). - rustls-pemfile 1.x / proc-macro-error2 unmaintained -- transitive, no runtime exposure, awaiting upstream. - time 0.3.45 / tokio-postgres 0.7.17 -- fixed releases require a newer Rust than the workspace MSRV (1.85); drop when the MSRV moves. * feat(backend): screen tenant sessions against cross-database SQL A session established through a ConnectionAuthenticator is tenant-scoped: the backend the authenticator returns is the whole world that connection may touch. But every tenant database is opened by one process under one uid, so ATTACH DATABASE '<other tenant file>' -- which passes MySQL-dialect translation verbatim -- reached the rusqlite backend and executed: cross-tenant read/write through an authenticated session. Only the Turso backend refused it, and only because that engine happens to. The MySQL frontend now wraps every authenticator-established session in litewire_backend::tenant_screen, which refuses ATTACH/DETACH, VACUUM with a target (VACUUM INTO), and the path-bearing or schema-reopening PRAGMAs (data_store_directory, temp_store_directory, writable_schema) with a clean statement-level SQL error, on every backend, whatever the engine underneath would do. The screen is quote-, comment-, and multi-statement-aware, sees through EXPLAIN / EXPLAIN QUERY PLAN wrappers and schema-qualified or quoted PRAGMA spellings, and treats malformed SQL (unterminated quote or block comment) conservatively as forbidden. Single-tenant sessions -- a fixed backend, or an embedder driving a BackendConn directly -- are deliberately not screened: ATTACH is legitimate in single-user embedded setups, and the screen keys off the session being tenant-scoped, never off the statement alone. A TenantScreened backend decorator is exported for embedders that want the screen on every route to a per-tenant backend, not only wire sessions. Wire-level regression tests (mysql_async against a two-tenant listener): the ATTACH attempt and each variation come back as SQL errors with the screen's own message, the session survives and still sees only its own data, and a fixed-backend server keeps full ATTACH freedom. The README multi-tenant section now states the guarantee precisely. * build(mysql): fence the opensrv-mysql tls feature so it cannot be shed silently opensrv-mysql calls authenticate() conditionally on the handshake username being present, and its parser yields None on the pre-TLS CLIENT_SSL branch. The tls feature (a default) is what makes that branch unreachable: init_after_ssl re-reads and re-parses the handshake, which produces a username. Dropping the feature -- a natural-looking default-features = false to trim dependencies -- would compile that re-parse out and make the no-username path reachable on the multi-tenant authenticating path, where authenticate() is the only tenant boundary. The handler fails closed structurally regardless (no successful authenticate() means no backend to reach), but the requirement should be enforced, not incidental. Two guards, per issue #32: - The workspace manifest declares features = ["tls"] explicitly, with a SECURITY comment, so disabling default features no longer drops it. - litewire-mysql imports the tls-gated secure_run_with_options re-export under a fence alias, so a build without the feature fails to compile -- litewire otherwise uses only ungated opensrv APIs and would have built silently. Closes #32.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three items from the security portion of the health audit (audited at cb707ab). Fixes #32.
1. RustSec advisories: fix what's fixable, gate the rest in CI
New
deny.toml+ acargo-denyCI job, so future advisories fail the build instead of accumulating.Fixed by version bumps:
LruCache::pop; 0.12.5 backs the productionTranslateCache) and RUSTSEC-2026-0002 (IterMutunsound).TranslateCacheneeded no code changes; its tests pass unchanged.lru 0.12copy and itscrossbeamsubtree from the lockfile.cargo update: anyhow 1.0.104 (RUSTSEC-2026-0190), crossbeam-epoch 0.9.20 (RUSTSEC-2026-0204, the turso_core path), postgres-protocol 0.6.12 (RUSTSEC-2026-0179/0180), rustls-webpki 0.103.14.Ignored — each with the reason and drop condition documented in
deny.toml, none silent:cargo deny checkis fully green locally:advisories ok, bans ok, licenses ok, sources ok.2. Tenant sessions can no longer reach past their own database file
The audit finding was real, and worse than "screens nothing":
ATTACH DATABASE '<path>'and path-bearingPRAGMAs pass MySQL-dialect translation verbatim, so on awith_authenticator+ rusqlite deployment an authenticated tenant could attach a neighbouring tenant's file and read/write it. Only the Turso backend refused, and only because that engine happens to.New
litewire_backend::tenant_screen: the MySQL frontend wraps every authenticator-established session in aBackendConndecorator that refusesATTACH/DETACH,VACUUMwith a target (VACUUM INTO '<path>'), andPRAGMA data_store_directory/temp_store_directory/writable_schemawith a clean statement-level SQL error, before the statement reaches any engine. Notes on the shape:ConnectionAuthenticator), applied at the one place tenancy is established — not off the backend type, since the authenticator returns an arbitrarySharedBackend. Rusqlite (the audited gap) is covered; so is every other backend, so a future engine that acceptsATTACHdoesn't reopen the hole.ATTACHincluded; bareVACUUMand tuning pragmas stay allowed on tenant sessions.EXPLAIN/EXPLAIN QUERY PLANwrappers and schema-qualified/quotedPRAGMAspellings (the known bypass classes for first-keyword checks), and treats malformed SQL conservatively as forbidden.describe_columns(theCOM_STMT_PREPAREpath) is screened like execution.TenantScreened<B>backend decorator exported for embedders that want the screen on every route to a per-tenant backend.Tests: 14 unit tests on the screen + wrapper (including the unscreened-session ATTACH-still-works contract), and wire-level regressions in
mysql_multi_tenant.rswith realmysql_asyncclients — tenantATTACHof the other tenant's actual file path comes back as the screen's own error and the connection survives; a fixed-backend server stillATTACHes over the wire. README multi-tenant section now states the guarantee precisely.3. opensrv-mysql
tlsfeature fence (#32)Exactly what the issue asks: the feature is now declared explicitly (
features = ["tls"]with a SECURITY comment explaining that it is load-bearing for authentication, not just TLS), plus a compile-time fence in litewire-mysql — an import of the tls-gatedsecure_run_with_optionsre-export under a fence alias. Verified: flipping the manifest todefault-features = falsefails the build at the fence (litewire's own code otherwise uses only ungated opensrv APIs and would have compiled silently).Verification
cargo test --workspace --all-features: 947 passed, 0 failed (default-features run also green)cargo clippy --workspace --all-targets -- -D warnings, and again with--all-features: cleancargo fmt --all -- --check: cleancargo deny check: green