Skip to content

Commit 2d39649

Browse files
committed
chore(mysql): create regression test for RUSTSEC-2024-0363
1 parent 2256d70 commit 2d39649

File tree

2 files changed

+69
-0
lines changed

2 files changed

+69
-0
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,11 @@ name = "mysql-migrate"
328328
path = "tests/mysql/migrate.rs"
329329
required-features = ["mysql", "macros", "migrate"]
330330

331+
[[test]]
332+
name = "mysql-rustsec"
333+
path = "tests/mysql/rustsec.rs"
334+
required-features = ["mysql"]
335+
331336
#
332337
# PostgreSQL
333338
#

tests/mysql/rustsec.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::io;
2+
use sqlx::{Error, MySql};
3+
4+
use sqlx_test::new;
5+
6+
// https://rustsec.org/advisories/RUSTSEC-2024-0363.html
7+
//
8+
// During the audit the MySQL driver was found to be *unlikely* to be vulnerable to the exploit,
9+
// so this just serves as a sanity check.
10+
#[sqlx::test]
11+
async fn rustsec_2024_0363() -> anyhow::Result<()> {
12+
let overflow_len = 4 * 1024 * 1024 * 1024; // 4 GiB
13+
14+
let padding = " ".repeat(overflow_len);
15+
16+
let payload = "UPDATE injection_target SET message = 'you''ve been pwned!' WHERE id = 1";
17+
18+
let mut injected_value = String::with_capacity(overflow_len + payload.len());
19+
20+
injected_value.push_str(&padding);
21+
injected_value.push_str(payload);
22+
23+
let mut conn = new::<MySql>().await?;
24+
25+
sqlx::raw_sql(
26+
"CREATE TEMPORARY TABLE injection_target(id INTEGER PRIMARY KEY AUTO_INCREMENT, message TEXT);\n\
27+
INSERT INTO injection_target(message) VALUES ('existing message');",
28+
)
29+
.execute(&mut conn)
30+
.await?;
31+
32+
// We can't concatenate a query string together like the other tests
33+
// because it would just demonstrate a regular old SQL injection.
34+
let res = sqlx::query("INSERT INTO injection_target(message) VALUES (?)")
35+
.bind(&injected_value)
36+
.execute(&mut conn)
37+
.await;
38+
39+
if let Err(e) = res {
40+
// Connection rejected the query; we're happy.
41+
//
42+
// Current observed behavior is that `mysqld` closes the connection before we're even done
43+
// sending the message, giving us a "Broken pipe" error.
44+
//
45+
// As it turns out, MySQL has a tight limit on packet sizes (even after splitting)
46+
// by default: https://dev.mysql.com/doc/refman/8.4/en/packet-too-large.html
47+
if matches!(e, Error::Io(ref ioe) if ioe.kind() == io::ErrorKind::BrokenPipe) {
48+
return Ok(());
49+
}
50+
51+
panic!("unexpected error: {e:?}");
52+
}
53+
54+
let messages: Vec<String> =
55+
sqlx::query_scalar("SELECT message FROM injection_target ORDER BY id")
56+
.fetch_all(&mut conn)
57+
.await?;
58+
59+
assert_eq!(messages[0], "existing_message");
60+
assert_eq!(messages[1].len(), injected_value.len());
61+
62+
// Injection didn't affect our database; we're happy.
63+
Ok(())
64+
}

0 commit comments

Comments
 (0)