Skip to content

Commit fc60ae7

Browse files
authored
Merge pull request #3503 from spinframework/allow-attach-file
sqlite-inproc: Add allow_attach_file runtime config option
2 parents bf54c5b + f6c284f commit fc60ae7

5 files changed

Lines changed: 50 additions & 9 deletions

File tree

crates/sqlite-inproc/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ edition = { workspace = true }
88
anyhow = { workspace = true }
99
async-trait = { workspace = true }
1010
futures = { workspace = true }
11-
rusqlite = { workspace = true, features = ["bundled"] }
11+
rusqlite = { workspace = true, features = ["bundled", "hooks"] }
1212
spin-factor-sqlite = { path = "../factor-sqlite" }
1313
spin-wasi-async = { path = "../wasi-async" }
1414
spin-world = { path = "../world" }

crates/sqlite-inproc/src/lib.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,19 @@ impl InProcDatabaseLocation {
4646
/// A connection to a sqlite database
4747
pub struct InProcConnection {
4848
location: InProcDatabaseLocation,
49+
allow_attach_file: bool,
4950
connection: OnceLock<Arc<Mutex<rusqlite::Connection>>>,
5051
}
5152

5253
impl InProcConnection {
53-
pub fn new(location: InProcDatabaseLocation) -> Result<Self, sqlite::Error> {
54+
pub fn new(
55+
location: InProcDatabaseLocation,
56+
allow_attach_file: bool,
57+
) -> Result<Self, sqlite::Error> {
5458
let connection = OnceLock::new();
5559
Ok(Self {
5660
location,
61+
allow_attach_file,
5762
connection,
5863
})
5964
}
@@ -74,6 +79,18 @@ impl InProcConnection {
7479
InProcDatabaseLocation::Path(path) => rusqlite::Connection::open(path),
7580
}
7681
.map_err(|e| sqlite::Error::Io(e.to_string()))?;
82+
if !self.allow_attach_file {
83+
connection.authorizer(Some(|ctx: rusqlite::hooks::AuthContext<'_>| {
84+
use rusqlite::hooks::{AuthAction, Authorization};
85+
match ctx.action {
86+
// Deny attaching files except tempfile ("") and in-memory (":memory:") databases
87+
AuthAction::Attach { filename } if !matches!(filename, "" | ":memory:") => {
88+
Authorization::Deny
89+
}
90+
_ => Authorization::Allow,
91+
}
92+
}));
93+
}
7794
Ok(Arc::new(Mutex::new(connection)))
7895
}
7996
}

crates/sqlite/src/lib.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ impl RuntimeConfigResolver {
126126
.map(|p| p.join(DEFAULT_SQLITE_DB_FILENAME));
127127
let factory = move || {
128128
let location = InProcDatabaseLocation::from_path(path.clone())?;
129-
let connection = spin_sqlite_inproc::InProcConnection::new(location)?;
129+
let connection = spin_sqlite_inproc::InProcConnection::new(location, false)?;
130130
Ok(Arc::new(connection) as _)
131131
};
132132
Arc::new(factory)
@@ -140,6 +140,13 @@ const DEFAULT_SQLITE_DB_FILENAME: &str = "sqlite_db.db";
140140
#[serde(deny_unknown_fields)]
141141
pub struct InProcDatabase {
142142
pub path: Option<PathBuf>,
143+
144+
/// If `false` (the default), disallows `ATTACH`ing an existing file to a
145+
/// database connection.
146+
///
147+
/// Note: Attaching a new tempfile or `:memory:` database is always allowed.
148+
#[serde(default)]
149+
pub allow_attach_file: bool,
143150
}
144151

145152
impl InProcDatabase {
@@ -156,7 +163,10 @@ impl InProcDatabase {
156163
.map(|p| resolve_relative_path(p, base_dir));
157164
let location = InProcDatabaseLocation::from_path(path)?;
158165
let factory = move || {
159-
let connection = spin_sqlite_inproc::InProcConnection::new(location.clone())?;
166+
let connection = spin_sqlite_inproc::InProcConnection::new(
167+
location.clone(),
168+
self.allow_attach_file,
169+
)?;
160170
Ok(Arc::new(connection) as _)
161171
};
162172
Ok(factory)

tests/test-components/components/sqlite/src/lib.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,23 @@ impl Component {
5353

5454
// This should exceed the 128MB query result limit:
5555
match conn.execute("SELECT * FROM test_data", &[]) {
56-
Ok(_) => bail!("large select should not have succeeded",),
56+
Ok(_) => bail!("large select should not have succeeded"),
5757
Err(Error::Io(s)) if s.contains("query result exceeds limit") => {}
58-
Err(e) => bail!("unexpected error: {e}",),
58+
Err(e) => bail!("unexpected error: {e}"),
59+
}
60+
61+
// ATTACH tempfile and in-memory are allowed
62+
for allowed_stmt in ["ATTACH '' AS tempfile", "ATTACH ':memory:' AS inmemory"] {
63+
conn.execute(allowed_stmt, &[])
64+
.map_err(|e| format!("{allowed_stmt:?} failed: {e:?}"))?;
65+
}
66+
// ATTACH <file> forbidden by default; VACUUM INTO uses ATTACH under the hood
67+
for forbidden_stmt in ["ATTACH 'any_file' AS attach_file", "VACUUM INTO 'any_file'"] {
68+
match conn.execute(forbidden_stmt, &[]) {
69+
Ok(_) => bail!("{forbidden_stmt:?} should fail"),
70+
Err(Error::Io(s)) if s.contains("authoriz") => {}
71+
Err(e) => bail!("unexpected error for {forbidden_stmt:?}: {e:?}"),
72+
}
5973
}
6074

6175
Ok(())

tests/test-components/helper/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ pub fn outgoing_body(body: OutgoingBody, buffer: Vec<u8>) -> Result<(), ErrorCod
109109
offset += count;
110110
}
111111
Err(e) => {
112-
return Err(ErrorCode::InternalError(Some(format!("I/O error: {e}"))))
112+
return Err(ErrorCode::InternalError(Some(format!("I/O error: {e}"))));
113113
}
114114
}
115115
}
@@ -174,12 +174,12 @@ macro_rules! ensure_eq {
174174

175175
#[macro_export]
176176
macro_rules! bail {
177-
($fmt:expr, $($arg:tt)*) => {{
177+
($fmt:expr $(, $($arg:tt)*)?) => {{
178178
let krate = module_path!().split("::").next().unwrap();
179179
let file = file!();
180180
let line = line!();
181181
return Err(format!(
182-
"{krate}#({file}:{line}) {}", format_args!($fmt, $($arg)*)
182+
"{krate}#({file}:{line}) {}", format_args!($fmt $(, $($arg)*)?)
183183
));
184184
}};
185185
}

0 commit comments

Comments
 (0)