fix(compat): open catalog databases with trusted_schema OFF
Security audit finding SEC-DB-01. The three read-only opens of the catalog `game.db` (runtime bundle loader, legacy ETI reader and the catalog publisher) set only `read_only(true)`. SQLite still honours schema-embedded SQL in that mode: triggers, views, CHECK constraints and expression indexes may call functions with side effects or virtual tables unless `trusted_schema` is off. `harden_read_only_catalog_options` now applies `trusted_schema = OFF` and `cell_size_check = ON` to those connections. The database is a bundled application resource, not a remote input, so this is defense in depth against a corrupted or tampered bundle; it has no effect on the parameterised queries the code runs. Test plan: `just test` (the compat tests open real fixture databases through the hardened options). Claude-Session: https://claude.ai/code/session_017C3Nbgwpdm3YNwZhhFLHwg
This commit is contained in:
@@ -47,6 +47,21 @@ impl LoadedCatalog {
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies defensive SQLite settings to a read-only catalog connection.
|
||||
///
|
||||
/// `trusted_schema = OFF` stops SQL functions with side effects and virtual
|
||||
/// tables from running out of triggers, views, CHECK constraints or indexes
|
||||
/// stored inside the database file, and `cell_size_check = ON` makes the
|
||||
/// b-tree layer validate cell sizes when reading pages. The catalog database
|
||||
/// ships inside the application bundle, so this is defense in depth against a
|
||||
/// corrupted or tampered resource rather than a remote input.
|
||||
#[must_use]
|
||||
pub fn harden_read_only_catalog_options(options: SqliteConnectOptions) -> SqliteConnectOptions {
|
||||
options
|
||||
.pragma("trusted_schema", "OFF")
|
||||
.pragma("cell_size_check", "ON")
|
||||
}
|
||||
|
||||
/// Loads the UI catalog and its exact content authority from one database
|
||||
/// snapshot.
|
||||
///
|
||||
@@ -64,9 +79,11 @@ pub async fn load_catalog_bundle(
|
||||
manifests_root: &Path,
|
||||
) -> eyre::Result<LoadedCatalog> {
|
||||
validate_regular_file(game_db_path, "catalog database")?;
|
||||
let options = SqliteConnectOptions::new()
|
||||
let options = harden_read_only_catalog_options(
|
||||
SqliteConnectOptions::new()
|
||||
.filename(game_db_path)
|
||||
.read_only(true);
|
||||
.read_only(true),
|
||||
);
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect_with(options)
|
||||
|
||||
@@ -24,7 +24,9 @@ pub struct CatalogGame {
|
||||
/// duplicate game IDs.
|
||||
pub async fn load_catalog_games(path: &Path) -> eyre::Result<BTreeMap<String, CatalogGame>> {
|
||||
validate_regular_file(path, "catalog database")?;
|
||||
let options = SqliteConnectOptions::new().filename(path).read_only(true);
|
||||
let options = crate::catalog_bundle::harden_read_only_catalog_options(
|
||||
SqliteConnectOptions::new().filename(path).read_only(true),
|
||||
);
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect_with(options)
|
||||
|
||||
@@ -23,7 +23,9 @@ pub struct EtiGame {
|
||||
|
||||
/// # Errors
|
||||
pub async fn get_games(db: &Path) -> eyre::Result<Vec<EtiGame>> {
|
||||
let options = SqliteConnectOptions::new().filename(db).read_only(true);
|
||||
let options = crate::catalog_bundle::harden_read_only_catalog_options(
|
||||
SqliteConnectOptions::new().filename(db).read_only(true),
|
||||
);
|
||||
let pool = SqlitePoolOptions::new().connect_with(options).await?;
|
||||
|
||||
let query_result = sqlx::query_as::<_, EtiGame>(
|
||||
|
||||
Reference in New Issue
Block a user