Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 22 additions & 0 deletions src-tauri/src/library/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2196,6 +2196,11 @@ impl Repository {
}

fn get_book_field(&self, book_id: &str, field_name: &str) -> anyhow::Result<Option<String>> {
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(
Expand All @@ -2207,6 +2212,11 @@ impl Repository {
}

fn get_book_i64_field(&self, book_id: &str, field_name: &str) -> anyhow::Result<Option<i64>> {
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(
Expand Down Expand Up @@ -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<Option<String>> {
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
Expand Down