Skip to content

Commit 3c62bd7

Browse files
committed
sqld: reject ATTACH statements
This adds support for rejecting `ATTACH` statements from dumps. `ATTACH` statements should be considered dangerous in a multi-tenant setting. With this commit any `ATTACH` statement inside a dump will be rejected with a 400 status code and a message. In addition, any other sql errors returned by sqlite will be returned as a 400. All other dump errors through this code path (conn.with_raw) will return a 500 like before.
1 parent a77f422 commit 3c62bd7

File tree

4 files changed

+109
-3
lines changed

4 files changed

+109
-3
lines changed

libsql-server/src/error.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ pub enum LoadDumpError {
297297
NoCommit,
298298
#[error("Path is not a file")]
299299
NotAFile,
300+
#[error("The passed dump sql is invalid: {0}")]
301+
InvalidSqlInput(String),
300302
}
301303

302304
impl ResponseError for LoadDumpError {}
@@ -315,7 +317,8 @@ impl IntoResponse for &LoadDumpError {
315317
| NoTxn
316318
| NoCommit
317319
| NotAFile
318-
| DumpFilePathNotAbsolute => self.format_err(StatusCode::BAD_REQUEST),
320+
| DumpFilePathNotAbsolute
321+
| InvalidSqlInput(_) => self.format_err(StatusCode::BAD_REQUEST),
319322
}
320323
}
321324
}

libsql-server/src/namespace/configurator/helpers.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use futures::Stream;
1111
use libsql_sys::EncryptionConfig;
1212
use libsql_wal::io::StdIO;
1313
use libsql_wal::registry::WalRegistry;
14+
use rusqlite::hooks::{AuthAction, AuthContext, Authorization};
1415
use tokio::io::AsyncBufReadExt as _;
1516
use tokio::task::JoinSet;
1617
use tokio_util::io::StreamReader;
@@ -328,8 +329,31 @@ where
328329
line = tokio::task::spawn_blocking({
329330
let conn = conn.clone();
330331
move || -> crate::Result<String, LoadDumpError> {
331-
conn.with_raw(|conn| conn.execute(&line, ())).map_err(|e| {
332-
LoadDumpError::Internal(format!("line: {}, error: {}", line_id, e))
332+
conn.with_raw(|conn| {
333+
conn.authorizer(Some(|auth: AuthContext<'_>| match auth.action {
334+
AuthAction::Attach { filename: _ } => Authorization::Deny,
335+
_ => Authorization::Allow,
336+
}));
337+
conn.execute(&line, ())
338+
})
339+
.map_err(|e| match e {
340+
rusqlite::Error::SqlInputError {
341+
msg, sql, offset, ..
342+
} => {
343+
let msg = if sql.to_lowercase().contains("attach") {
344+
format!(
345+
"attach statements are not allowed in dumps, msg: {}, sql: {}, offset: {}",
346+
msg,
347+
sql,
348+
offset
349+
)
350+
} else {
351+
format!("msg: {}, sql: {}, offset: {}", msg, sql, offset)
352+
};
353+
354+
LoadDumpError::InvalidSqlInput(msg)
355+
}
356+
e => LoadDumpError::Internal(format!("line: {}, error: {}", line_id, e)),
333357
})?;
334358
Ok(line)
335359
}

libsql-server/tests/namespaces/dumps.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,76 @@ fn export_dump() {
279279

280280
sim.run().unwrap();
281281
}
282+
283+
#[test]
284+
fn load_dump_with_attach_rejected() {
285+
const DUMP: &str = r#"
286+
PRAGMA foreign_keys=OFF;
287+
BEGIN TRANSACTION;
288+
CREATE TABLE test (x);
289+
INSERT INTO test VALUES(42);
290+
ATTACH foo/bar.sql
291+
COMMIT;"#;
292+
293+
let mut sim = Builder::new()
294+
.simulation_duration(Duration::from_secs(1000))
295+
.build();
296+
let tmp = tempdir().unwrap();
297+
let tmp_path = tmp.path().to_path_buf();
298+
299+
std::fs::write(tmp_path.join("dump.sql"), DUMP).unwrap();
300+
301+
make_primary(&mut sim, tmp.path().to_path_buf());
302+
303+
sim.client("client", async move {
304+
let client = Client::new();
305+
306+
// path is not absolute is an error
307+
let resp = client
308+
.post(
309+
"http://primary:9090/v1/namespaces/foo/create",
310+
json!({ "dump_url": "file:dump.sql"}),
311+
)
312+
.await
313+
.unwrap();
314+
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
315+
316+
// path doesn't exist is an error
317+
let resp = client
318+
.post(
319+
"http://primary:9090/v1/namespaces/foo/create",
320+
json!({ "dump_url": "file:/dump.sql"}),
321+
)
322+
.await
323+
.unwrap();
324+
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
325+
326+
let resp = client
327+
.post(
328+
"http://primary:9090/v1/namespaces/foo/create",
329+
json!({ "dump_url": format!("file:{}", tmp_path.join("dump.sql").display())}),
330+
)
331+
.await
332+
.unwrap();
333+
assert_eq!(
334+
resp.status(),
335+
StatusCode::BAD_REQUEST,
336+
"{}",
337+
resp.json::<serde_json::Value>().await.unwrap_or_default()
338+
);
339+
340+
assert_snapshot!(resp.body_string().await?);
341+
342+
let foo =
343+
Database::open_remote_with_connector("http://foo.primary:8080", "", TurmoilConnector)?;
344+
let foo_conn = foo.connect()?;
345+
346+
let res = foo_conn.query("select count(*) from test", ()).await;
347+
// This should error since the dump should have failed!
348+
assert!(res.is_err());
349+
350+
Ok(())
351+
});
352+
353+
sim.run().unwrap();
354+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
source: libsql-server/tests/namespaces/dumps.rs
3+
expression: resp.body_string().await?
4+
snapshot_kind: text
5+
---
6+
{"error":"The passed dump sql is invalid: attach statements are not allowed in dumps, msg: near \"COMMIT\": syntax error, sql: ATTACH foo/bar.sql\n COMMIT;, offset: 28"}

0 commit comments

Comments
 (0)