diff --git a/Cargo.toml b/Cargo.toml index 58c66388d9..e50eb48b86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -328,6 +328,11 @@ name = "mysql-migrate" path = "tests/mysql/migrate.rs" required-features = ["mysql", "macros", "migrate"] +[[test]] +name = "mysql-rustsec" +path = "tests/mysql/rustsec.rs" +required-features = ["mysql"] + # # PostgreSQL # diff --git a/tests/mysql/rustsec.rs b/tests/mysql/rustsec.rs new file mode 100644 index 0000000000..8d8db0c250 --- /dev/null +++ b/tests/mysql/rustsec.rs @@ -0,0 +1,67 @@ +use sqlx::{Error, MySql}; +use std::io; + +use sqlx_test::new; + +// https://rustsec.org/advisories/RUSTSEC-2024-0363.html +// +// During the audit the MySQL driver was found to be *unlikely* to be vulnerable to the exploit, +// so this just serves as a sanity check. +#[sqlx::test] +async fn rustsec_2024_0363() -> anyhow::Result<()> { + let overflow_len = 4 * 1024 * 1024 * 1024; // 4 GiB + + let padding = " ".repeat(overflow_len); + + let payload = "UPDATE injection_target SET message = 'you''ve been pwned!' WHERE id = 1"; + + let mut injected_value = String::with_capacity(overflow_len + payload.len()); + + injected_value.push_str(&padding); + injected_value.push_str(payload); + + // Since this is so large, keeping it around until the end *can* lead to getting OOM-killed. + drop(padding); + + let mut conn = new::().await?; + + sqlx::raw_sql( + "CREATE TEMPORARY TABLE injection_target(id INTEGER PRIMARY KEY AUTO_INCREMENT, message TEXT);\n\ + INSERT INTO injection_target(message) VALUES ('existing message');", + ) + .execute(&mut conn) + .await?; + + // We can't concatenate a query string together like the other tests + // because it would just demonstrate a regular old SQL injection. + let res = sqlx::query("INSERT INTO injection_target(message) VALUES (?)") + .bind(&injected_value) + .execute(&mut conn) + .await; + + if let Err(e) = res { + // Connection rejected the query; we're happy. + // + // Current observed behavior is that `mysqld` closes the connection before we're even done + // sending the message, giving us a "Broken pipe" error. + // + // As it turns out, MySQL has a tight limit on packet sizes (even after splitting) + // by default: https://dev.mysql.com/doc/refman/8.4/en/packet-too-large.html + if matches!(e, Error::Io(ref ioe) if ioe.kind() == io::ErrorKind::BrokenPipe) { + return Ok(()); + } + + panic!("unexpected error: {e:?}"); + } + + let messages: Vec = + sqlx::query_scalar("SELECT message FROM injection_target ORDER BY id") + .fetch_all(&mut conn) + .await?; + + assert_eq!(messages[0], "existing_message"); + assert_eq!(messages[1].len(), injected_value.len()); + + // Injection didn't affect our database; we're happy. + Ok(()) +}