diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..a63bbb8 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-02-14 - Fix Potential SQL Injection in Dynamic DB Queries +**Vulnerability:** SQL Injection in SQLite parameter retrieval logic. +**Learning:** The Rust logic for getting specific fields from the books table used dynamic string interpolation (`format!("SELECT {field_name} FROM books ...")`) rather than SQL binding. The `field_name` could potentially be manipulated by an attacker depending on upstream processing, which could cause unwanted SQL execution. +**Prevention:** Avoid dynamic column name interpolation in SQL whenever possible. If it must be done, implement a strict enum or string match allowlist for safety to prevent arbitrary column lookups. diff --git a/src-tauri/src/library/db.rs b/src-tauri/src/library/db.rs index ebc369f..006b26d 100644 --- a/src-tauri/src/library/db.rs +++ b/src-tauri/src/library/db.rs @@ -2196,6 +2196,11 @@ impl Repository { } fn get_book_field(&self, book_id: &str, field_name: &str) -> anyhow::Result> { + anyhow::ensure!( + is_safe_book_column(field_name), + "Invalid column name: {}", + field_name + ); let conn = self.conn()?; let query = format!("SELECT {field_name} FROM books WHERE id = ?1"); Ok( @@ -2207,6 +2212,11 @@ impl Repository { } fn get_book_i64_field(&self, book_id: &str, field_name: &str) -> anyhow::Result> { + anyhow::ensure!( + is_safe_book_column(field_name), + "Invalid column name: {}", + field_name + ); let conn = self.conn()?; let query = format!("SELECT {field_name} FROM books WHERE id = ?1"); Ok( @@ -2310,11 +2320,23 @@ fn metadata_field_to_override_name(field: &MetadataField) -> Option<&'static str metadata_field_to_book_column(field) } +fn is_safe_book_column(field_name: &str) -> bool { + matches!( + field_name, + "id" | "title" | "subtitle" | "authors_json" | "publisher" | "publish_date" | "isbn10" | "isbn13" | "description" | "language" | "page_count" | "series" | "series_index" | "cover_url" | "cover_local_path" + ) +} + fn get_book_field_value_for_lock( tx: &rusqlite::Transaction<'_>, book_id: &str, field_name: &str, ) -> anyhow::Result> { + anyhow::ensure!( + is_safe_book_column(field_name), + "Invalid column name: {}", + field_name + ); let query = format!("SELECT {field_name} FROM books WHERE id = ?1 LIMIT 1"); if matches!(field_name, "page_count" | "series_index") { let numeric = tx