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
|
/// Loads the UI catalog and its exact content authority from one database
|
||||||
/// snapshot.
|
/// snapshot.
|
||||||
///
|
///
|
||||||
@@ -64,9 +79,11 @@ pub async fn load_catalog_bundle(
|
|||||||
manifests_root: &Path,
|
manifests_root: &Path,
|
||||||
) -> eyre::Result<LoadedCatalog> {
|
) -> eyre::Result<LoadedCatalog> {
|
||||||
validate_regular_file(game_db_path, "catalog database")?;
|
validate_regular_file(game_db_path, "catalog database")?;
|
||||||
let options = SqliteConnectOptions::new()
|
let options = harden_read_only_catalog_options(
|
||||||
.filename(game_db_path)
|
SqliteConnectOptions::new()
|
||||||
.read_only(true);
|
.filename(game_db_path)
|
||||||
|
.read_only(true),
|
||||||
|
);
|
||||||
let pool = SqlitePoolOptions::new()
|
let pool = SqlitePoolOptions::new()
|
||||||
.max_connections(1)
|
.max_connections(1)
|
||||||
.connect_with(options)
|
.connect_with(options)
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ pub struct CatalogGame {
|
|||||||
/// duplicate game IDs.
|
/// duplicate game IDs.
|
||||||
pub async fn load_catalog_games(path: &Path) -> eyre::Result<BTreeMap<String, CatalogGame>> {
|
pub async fn load_catalog_games(path: &Path) -> eyre::Result<BTreeMap<String, CatalogGame>> {
|
||||||
validate_regular_file(path, "catalog database")?;
|
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()
|
let pool = SqlitePoolOptions::new()
|
||||||
.max_connections(1)
|
.max_connections(1)
|
||||||
.connect_with(options)
|
.connect_with(options)
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ pub struct EtiGame {
|
|||||||
|
|
||||||
/// # Errors
|
/// # Errors
|
||||||
pub async fn get_games(db: &Path) -> eyre::Result<Vec<EtiGame>> {
|
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 pool = SqlitePoolOptions::new().connect_with(options).await?;
|
||||||
|
|
||||||
let query_result = sqlx::query_as::<_, EtiGame>(
|
let query_result = sqlx::query_as::<_, EtiGame>(
|
||||||
|
|||||||
Reference in New Issue
Block a user