Skip to content

Security batch: cargo-deny gate + lru UAF fix, tenant-session SQL screen, opensrv tls fence - #34

Merged
luthermonson merged 3 commits into
mainfrom
sec/audit-batch
Aug 16, 2026
Merged

Security batch: cargo-deny gate + lru UAF fix, tenant-session SQL screen, opensrv tls fence#34
luthermonson merged 3 commits into
mainfrom
sec/audit-batch

Conversation

@luthermonson

Copy link
Copy Markdown
Contributor

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 + a cargo-deny CI job, so future advisories fail the build instead of accumulating.

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). TranslateCache needed no code changes; its tests pass unchanged.
  • mysql_async 0.34 -> 0.37 (dev-only e2e client) — removes the second lru 0.12 copy and its crossbeam subtree 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:

  • rustls-webpki 0.102.8 / 0.101.7 CRL + name-constraint advisories: old copies pinned under opensrv-mysql (server-side TLS, paths not exercised) and tiberius (dev-only TDS test client).
  • rustls-pemfile 1.x, proc-macro-error2: unmaintained, transitive, no runtime exposure.
  • time 0.3.45, tokio-postgres 0.7.17: fixed releases are blocked by the workspace MSRV (1.85); the MSRV-aware resolver refuses them. Drop when the MSRV moves.

cargo deny check is 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-bearing PRAGMAs pass MySQL-dialect translation verbatim, so on a with_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 a BackendConn decorator that refuses ATTACH/DETACH, VACUUM with a target (VACUUM INTO '<path>'), and PRAGMA data_store_directory / temp_store_directory / writable_schema with a clean statement-level SQL error, before the statement reaches any engine. Notes on the shape:

  • It keys off the session being tenant-scoped (established through a ConnectionAuthenticator), applied at the one place tenancy is established — not off the backend type, since the authenticator returns an arbitrary SharedBackend. Rusqlite (the audited gap) is covered; so is every other backend, so a future engine that accepts ATTACH doesn't reopen the hole.
  • Single-tenant / no-auth usage keeps full SQL freedom, ATTACH included; bare VACUUM and tuning pragmas stay allowed on tenant sessions.
  • The screen is quote-, comment-, and multi-statement-aware, sees through EXPLAIN / EXPLAIN QUERY PLAN wrappers and schema-qualified/quoted PRAGMA spellings (the known bypass classes for first-keyword checks), and treats malformed SQL conservatively as forbidden. describe_columns (the COM_STMT_PREPARE path) 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.rs with real mysql_async clients — tenant ATTACH of the other tenant's actual file path comes back as the screen's own error and the connection survives; a fixed-backend server still ATTACHes over the wire. README multi-tenant section now states the guarantee precisely.

3. opensrv-mysql tls feature 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-gated secure_run_with_options re-export under a fence alias. Verified: flipping the manifest to default-features = false fails 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: clean
  • cargo fmt --all -- --check: clean
  • cargo deny check: green

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
luthermonson merged commit 94fd724 into main Aug 16, 2026
4 checks passed
@luthermonson
luthermonson deleted the sec/audit-batch branch August 16, 2026 17:03
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Guard the opensrv-mysql tls feature — default-features = false would expose the CLIENT_SSL no-username path

1 participant