Skip to content

Commit e450e2e

Browse files
committed
fix: harden SQL identifier handling against injection
These were not exploitable vulnerabilities but best practice is to fix them Use the pragma_table_info() table-valued function in the observer crate so the table name is bound as a parameter rather than interpolated into the SQL string. Quote schema names in DETACH DATABASE statements in the conn-mgr crate for defense-in-depth alongside the existing allowlist validation.
1 parent 91b7379 commit e450e2e

2 files changed

Lines changed: 20 additions & 22 deletions

File tree

crates/sqlx-sqlite-conn-mgr/src/attached.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ impl AttachedReadConnection {
7070
/// attached databases may persist when the connection is returned to the pool.
7171
pub async fn detach_all(mut self) -> Result<()> {
7272
for schema_name in &self.schema_names {
73-
let detach_sql = format!("DETACH DATABASE {}", schema_name);
73+
let detach_sql = format!("DETACH DATABASE \"{}\"", schema_name);
7474
sqlx::query(&detach_sql).execute(&mut *self.conn).await?;
7575
}
7676
Ok(())
@@ -140,7 +140,7 @@ impl AttachedWriteGuard {
140140
/// attached databases may persist when the connection is returned to the pool.
141141
pub async fn detach_all(mut self) -> Result<()> {
142142
for schema_name in &self.schema_names {
143-
let detach_sql = format!("DETACH DATABASE {}", schema_name);
143+
let detach_sql = format!("DETACH DATABASE \"{}\"", schema_name);
144144
sqlx::query(&detach_sql).execute(&mut *self.writer).await?;
145145
}
146146
Ok(())
@@ -252,7 +252,10 @@ pub async fn acquire_reader_with_attached(
252252
// Schema name is validated above to contain only safe identifier characters
253253
let path = spec.database.path_str();
254254
let escaped_path = path.replace("'", "''");
255-
let attach_sql = format!("ATTACH DATABASE '{}' AS {}", escaped_path, spec.schema_name);
255+
let attach_sql = format!(
256+
"ATTACH DATABASE '{}' AS \"{}\"",
257+
escaped_path, spec.schema_name
258+
);
256259
sqlx::query(&attach_sql).execute(&mut *conn).await?;
257260

258261
schema_names.push(spec.schema_name);
@@ -349,7 +352,10 @@ pub async fn acquire_writer_with_attached(
349352
for spec in specs {
350353
let path = spec.database.path_str();
351354
let escaped_path = path.replace("'", "''");
352-
let attach_sql = format!("ATTACH DATABASE '{}' AS {}", escaped_path, spec.schema_name);
355+
let attach_sql = format!(
356+
"ATTACH DATABASE '{}' AS \"{}\"",
357+
escaped_path, spec.schema_name
358+
);
353359
sqlx::query(&attach_sql).execute(&mut *writer).await?;
354360

355361
schema_names.push(spec.schema_name);

crates/sqlx-sqlite-observer/src/schema.rs

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ pub async fn query_table_info(
2020
// Check if table exists and get WITHOUT ROWID status
2121
let without_rowid = is_without_rowid(conn, table_name).await?;
2222

23-
// Get primary key columns using PRAGMA table_info
23+
// Get primary key columns using pragma_table_info()
2424
let pk_columns = query_pk_columns(conn, table_name).await?;
2525

2626
// Determine if table exists:
27-
// - If pk_columns is None, PRAGMA table_info returned no rows (table doesn't exist)
27+
// - If pk_columns is None, pragma_table_info returned no rows (table doesn't exist)
2828
// - If without_rowid is true, the table must exist (we found it in sqlite_master)
2929
// - A table with no explicit PK returns Some([]), not None
3030
if pk_columns.is_none() && !without_rowid {
@@ -78,15 +78,20 @@ fn has_without_rowid_clause(create_sql: &str) -> bool {
7878
/// Returns column indices in the order they appear in the PRIMARY KEY definition.
7979
/// For composite primary keys, the `pk` column in PRAGMA table_info indicates
8080
/// the position (1-indexed) within the PK.
81+
///
82+
/// Uses the `pragma_table_info()` table-valued function (available since SQLite
83+
/// 3.16.0) so the table name can be bound as a parameter instead of interpolated
84+
/// into the SQL string.
8185
async fn query_pk_columns(
8286
conn: &mut SqliteConnection,
8387
table_name: &str,
8488
) -> crate::Result<Option<Vec<usize>>> {
85-
// PRAGMA table_info returns: cid, name, type, notnull, dflt_value, pk
89+
// pragma_table_info returns: cid, name, type, notnull, dflt_value, pk
8690
// pk is 0 for non-PK columns, or 1-indexed position for PK columns
87-
let pragma = format!("PRAGMA table_info({})", quote_identifier(table_name));
91+
let sql = "SELECT cid, name, type, \"notnull\", dflt_value, pk FROM pragma_table_info(?1)";
8892

89-
let rows = sqlx::query(&pragma)
93+
let rows = sqlx::query(sql)
94+
.bind(table_name)
9095
.fetch_all(&mut *conn)
9196
.await
9297
.map_err(crate::Error::Sqlx)?;
@@ -116,23 +121,10 @@ async fn query_pk_columns(
116121
Ok(Some(pk_columns.into_iter().map(|(cid, _)| cid).collect()))
117122
}
118123

119-
/// Quotes a SQLite identifier to prevent SQL injection.
120-
fn quote_identifier(name: &str) -> String {
121-
// Double any existing double quotes and wrap in double quotes
122-
format!("\"{}\"", name.replace('"', "\"\""))
123-
}
124-
125124
#[cfg(test)]
126125
mod tests {
127126
use super::*;
128127

129-
#[test]
130-
fn test_quote_identifier() {
131-
assert_eq!(quote_identifier("users"), "\"users\"");
132-
assert_eq!(quote_identifier("my table"), "\"my table\"");
133-
assert_eq!(quote_identifier("foo\"bar"), "\"foo\"\"bar\"");
134-
}
135-
136128
#[test]
137129
fn test_has_without_rowid_clause() {
138130
// Positive cases

0 commit comments

Comments
 (0)