Skip to content

Commit ab4fa06

Browse files
committed
chore(sqlite): create regression test for RUSTSEC-2024-0363
1 parent fffb08f commit ab4fa06

File tree

2 files changed

+83
-0
lines changed

2 files changed

+83
-0
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ name = "sqlite-migrate"
273273
path = "tests/sqlite/migrate.rs"
274274
required-features = ["sqlite", "macros", "migrate"]
275275

276+
[[test]]
277+
name = "sqlite-rustsec"
278+
path = "tests/sqlite/rustsec.rs"
279+
required-features = ["sqlite"]
280+
276281
[[bench]]
277282
name = "sqlite-describe"
278283
path = "benches/sqlite/describe.rs"

tests/sqlite/rustsec.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use sqlx::{Connection, Error, SqliteConnection};
2+
3+
// https://rustsec.org/advisories/RUSTSEC-2024-0363.html
4+
//
5+
// Similar theory to the Postgres exploit in `tests/postgres/rustsec.rs` but much simpler
6+
// since we just want to overflow the query length itself.
7+
#[sqlx::test]
8+
async fn rustsec_2024_0363() -> anyhow::Result<()> {
9+
let overflow_len = 4 * 1024 * 1024 * 1024; // 4 GiB
10+
11+
// `real_query_prefix` plus `fake_message` will be the first query that SQLite "sees"
12+
//
13+
// Rather contrived because this already represents a regular SQL injection,
14+
// but this is the easiest way to demonstrate the exploit for SQLite.
15+
let real_query_prefix = "INSERT INTO injection_target(message) VALUES ('";
16+
let fake_message = "fake_msg') RETURNING id;";
17+
let real_query_suffix = "') RETURNING id";
18+
19+
// Our actual payload is another query
20+
let real_payload =
21+
"\nUPDATE injection_target SET message = 'you''ve been pwned!' WHERE id = 1;\n--";
22+
23+
// This will parse the query up to `real_payload`.
24+
let fake_payload_len = real_query_prefix.len() + fake_message.len();
25+
26+
// Pretty easy to see that this will overflow to `fake_payload_len`
27+
let target_len = overflow_len + fake_payload_len;
28+
29+
let inject_len = target_len - real_query_prefix.len() - real_query_suffix.len();
30+
31+
let pad_len = inject_len - fake_message.len() - real_payload.len();
32+
33+
let mut injected_value = String::with_capacity(inject_len);
34+
injected_value.push_str(fake_message);
35+
injected_value.push_str(real_payload);
36+
37+
let padding = " ".repeat(pad_len);
38+
injected_value.push_str(&padding);
39+
40+
let query = format!("{real_query_prefix}{injected_value}{real_query_suffix}");
41+
42+
assert_eq!(query.len(), target_len);
43+
44+
let mut conn = SqliteConnection::connect("sqlite://:memory:").await?;
45+
46+
sqlx::raw_sql(
47+
"CREATE TABLE injection_target(id INTEGER PRIMARY KEY, message TEXT);\n\
48+
INSERT INTO injection_target(message) VALUES ('existing message');",
49+
)
50+
.execute(&mut conn)
51+
.await?;
52+
53+
let res = sqlx::raw_sql(&query).execute(&mut conn).await;
54+
55+
if let Err(e) = res {
56+
// Connection rejected the query; we're happy.
57+
if matches!(e, Error::Protocol(_)) {
58+
return Ok(());
59+
}
60+
61+
panic!("unexpected error: {e:?}");
62+
}
63+
64+
let messages: Vec<String> =
65+
sqlx::query_scalar("SELECT message FROM injection_target ORDER BY id")
66+
.fetch_all(&mut conn)
67+
.await?;
68+
69+
// If the injection succeeds, `messages` will look like:
70+
// ["you've been pwned!'.to_string(), "fake_msg".to_string()]
71+
assert_eq!(
72+
messages,
73+
["existing message".to_string(), "fake_msg".to_string()]
74+
);
75+
76+
// Injection didn't affect our database; we're happy.
77+
Ok(())
78+
}

0 commit comments

Comments
 (0)