Skip to content

Commit 33b2777

Browse files
authored
Merge pull request #1841 from tursodatabase/lucio/fix-attach-dump
sqld: reject `ATTACH` statements
2 parents 54d3bdc + 8f0f58c commit 33b2777

File tree

5 files changed

+188
-3
lines changed

5 files changed

+188
-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: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,149 @@ 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+
}
355+
356+
#[test]
357+
fn load_dump_with_invalid_sql() {
358+
const DUMP: &str = r#"
359+
PRAGMA foreign_keys=OFF;
360+
BEGIN TRANSACTION;
361+
CREATE TABLE test (x);
362+
INSERT INTO test VALUES(42);
363+
SELECT abs(-9223372036854775808)
364+
COMMIT;"#;
365+
366+
let mut sim = Builder::new()
367+
.simulation_duration(Duration::from_secs(1000))
368+
.build();
369+
let tmp = tempdir().unwrap();
370+
let tmp_path = tmp.path().to_path_buf();
371+
372+
std::fs::write(tmp_path.join("dump.sql"), DUMP).unwrap();
373+
374+
make_primary(&mut sim, tmp.path().to_path_buf());
375+
376+
sim.client("client", async move {
377+
let client = Client::new();
378+
379+
// path is not absolute is an error
380+
let resp = client
381+
.post(
382+
"http://primary:9090/v1/namespaces/foo/create",
383+
json!({ "dump_url": "file:dump.sql"}),
384+
)
385+
.await
386+
.unwrap();
387+
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
388+
389+
// path doesn't exist is an error
390+
let resp = client
391+
.post(
392+
"http://primary:9090/v1/namespaces/foo/create",
393+
json!({ "dump_url": "file:/dump.sql"}),
394+
)
395+
.await
396+
.unwrap();
397+
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
398+
399+
let resp = client
400+
.post(
401+
"http://primary:9090/v1/namespaces/foo/create",
402+
json!({ "dump_url": format!("file:{}", tmp_path.join("dump.sql").display())}),
403+
)
404+
.await
405+
.unwrap();
406+
assert_eq!(
407+
resp.status(),
408+
StatusCode::BAD_REQUEST,
409+
"{}",
410+
resp.json::<serde_json::Value>().await.unwrap_or_default()
411+
);
412+
413+
assert_snapshot!(resp.body_string().await?);
414+
415+
let foo =
416+
Database::open_remote_with_connector("http://foo.primary:8080", "", TurmoilConnector)?;
417+
let foo_conn = foo.connect()?;
418+
419+
let res = foo_conn.query("select count(*) from test", ()).await;
420+
// This should error since the dump should have failed!
421+
assert!(res.is_err());
422+
423+
Ok(())
424+
});
425+
426+
sim.run().unwrap();
427+
}
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"}
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: msg: near \"COMMIT\": syntax error, sql: SELECT abs(-9223372036854775808) \n COMMIT;, offset: 43"}

0 commit comments

Comments
 (0)