diff --git a/sqlx-mysql/src/connection/auth.rs b/sqlx-mysql/src/connection/auth.rs index c533c90812..dce04cf843 100644 --- a/sqlx-mysql/src/connection/auth.rs +++ b/sqlx-mysql/src/connection/auth.rs @@ -27,6 +27,12 @@ impl AuthPlugin { // https://mariadb.com/kb/en/sha256_password-plugin/ AuthPlugin::Sha256Password => encrypt_rsa(stream, 0x01, password, nonce).await, + + AuthPlugin::MySqlClearPassword => { + let mut pw_bytes = password.as_bytes().to_owned(); + pw_bytes.push(0); // null terminate + Ok(pw_bytes) + } } } diff --git a/sqlx-mysql/src/connection/establish.rs b/sqlx-mysql/src/connection/establish.rs index 75838c57f5..d7ffc048f7 100644 --- a/sqlx-mysql/src/connection/establish.rs +++ b/sqlx-mysql/src/connection/establish.rs @@ -11,7 +11,7 @@ use crate::protocol::connect::{ AuthSwitchRequest, AuthSwitchResponse, Handshake, HandshakeResponse, }; use crate::protocol::Capabilities; -use crate::{MySqlConnectOptions, MySqlConnection}; +use crate::{MySqlConnectOptions, MySqlConnection, MySqlSslMode}; impl MySqlConnection { pub(crate) async fn establish(options: &MySqlConnectOptions) -> Result { @@ -49,6 +49,15 @@ impl<'a> DoHandshake<'a> { .transpose()? .unwrap_or_else(|| charset.default_collation()); + if options.enable_cleartext_plugin + && matches!( + options.ssl_mode, + MySqlSslMode::Disabled | MySqlSslMode::Preferred + ) + { + log::warn!("Security warning: sending cleartext passwords without requiring SSL"); + } + Ok(Self { options, charset, @@ -134,7 +143,8 @@ impl<'a> DoHandshake<'a> { } 0xfe => { - let switch: AuthSwitchRequest = packet.decode()?; + let switch: AuthSwitchRequest = + packet.decode_with(self.options.enable_cleartext_plugin)?; plugin = Some(switch.plugin); let nonce = switch.data.chain(Bytes::new()); diff --git a/sqlx-mysql/src/options/mod.rs b/sqlx-mysql/src/options/mod.rs index dc06380b07..0585f7cfe7 100644 --- a/sqlx-mysql/src/options/mod.rs +++ b/sqlx-mysql/src/options/mod.rs @@ -68,6 +68,7 @@ pub struct MySqlConnectOptions { pub(crate) collation: Option, pub(crate) log_settings: LogSettings, pub(crate) pipes_as_concat: bool, + pub(crate) enable_cleartext_plugin: bool, } impl Default for MySqlConnectOptions { @@ -95,6 +96,7 @@ impl MySqlConnectOptions { statement_cache_capacity: 100, log_settings: Default::default(), pipes_as_concat: true, + enable_cleartext_plugin: false, } } @@ -258,4 +260,19 @@ impl MySqlConnectOptions { self.pipes_as_concat = flag_val; self } + + /// Enables mysql_clear_password plugin support. + /// + /// Security Note: + /// Sending passwords as cleartext may be a security problem in some + /// configurations. Without additional defensive configuration like + /// ssl-mode=VERIFY_IDENTITY, an attacker can compromise a router + /// and trick the application into divulging its credentials. + /// + /// It is strongly recommended to set `.ssl_mode` to `Required`, + /// `VerifyCa`, or `VerifyIdentity` when enabling cleartext plugin. + pub fn enable_cleartext_plugin(mut self, flag_val: bool) -> Self { + self.enable_cleartext_plugin = flag_val; + self + } } diff --git a/sqlx-mysql/src/protocol/auth.rs b/sqlx-mysql/src/protocol/auth.rs index 261a8817ab..091831e506 100644 --- a/sqlx-mysql/src/protocol/auth.rs +++ b/sqlx-mysql/src/protocol/auth.rs @@ -7,6 +7,7 @@ pub enum AuthPlugin { MySqlNativePassword, CachingSha2Password, Sha256Password, + MySqlClearPassword, } impl AuthPlugin { @@ -15,6 +16,7 @@ impl AuthPlugin { AuthPlugin::MySqlNativePassword => "mysql_native_password", AuthPlugin::CachingSha2Password => "caching_sha2_password", AuthPlugin::Sha256Password => "sha256_password", + AuthPlugin::MySqlClearPassword => "mysql_clear_password", } } } @@ -27,6 +29,7 @@ impl FromStr for AuthPlugin { "mysql_native_password" => Ok(AuthPlugin::MySqlNativePassword), "caching_sha2_password" => Ok(AuthPlugin::CachingSha2Password), "sha256_password" => Ok(AuthPlugin::Sha256Password), + "mysql_clear_password" => Ok(AuthPlugin::MySqlClearPassword), _ => Err(err_protocol!("unknown authentication plugin: {}", s)), } diff --git a/sqlx-mysql/src/protocol/connect/auth_switch.rs b/sqlx-mysql/src/protocol/connect/auth_switch.rs index 0c25a42500..58b7fbb2ef 100644 --- a/sqlx-mysql/src/protocol/connect/auth_switch.rs +++ b/sqlx-mysql/src/protocol/connect/auth_switch.rs @@ -14,8 +14,8 @@ pub struct AuthSwitchRequest { pub data: Bytes, } -impl Decode<'_> for AuthSwitchRequest { - fn decode_with(mut buf: Bytes, _: ()) -> Result { +impl Decode<'_, bool> for AuthSwitchRequest { + fn decode_with(mut buf: Bytes, enable_cleartext_plugin: bool) -> Result { let header = buf.get_u8(); if header != 0xfe { return Err(err_protocol!( @@ -26,6 +26,21 @@ impl Decode<'_> for AuthSwitchRequest { let plugin = buf.get_str_nul()?.parse()?; + if matches!(plugin, AuthPlugin::MySqlClearPassword) && !enable_cleartext_plugin { + return Err(err_protocol!("mysql_cleartext_plugin disabled")); + } + + if matches!(plugin, AuthPlugin::MySqlClearPassword) && buf.is_empty() { + // Contrary to the MySQL protocol, AWS Aurora with IAM sends + // no data. That is fine because the mysql_clear_password says to + // ignore any data sent. + // See: https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_authentication_methods_clear_text_password.html + return Ok(Self { + plugin, + data: Bytes::new(), + }); + } + // See: https://github.com/mysql/mysql-server/blob/ea7d2e2d16ac03afdd9cb72a972a95981107bf51/sql/auth/sha2_password.cc#L942 if buf.len() != 21 { return Err(err_protocol!( @@ -48,3 +63,35 @@ impl Encode<'_, Capabilities> for AuthSwitchResponse { buf.extend_from_slice(&self.0); } } + +#[test] +fn test_decode_auth_switch_packet_data() { + const AUTH_SWITCH_NO_DATA: &[u8] = b"\xfecaching_sha2_password\x00abcdefghijabcdefghij\x00"; + + let p = AuthSwitchRequest::decode_with(AUTH_SWITCH_NO_DATA.into(), true).unwrap(); + + assert!(matches!(p.plugin, AuthPlugin::CachingSha2Password)); + assert_eq!(p.data, &b"abcdefghijabcdefghij"[..]); +} + +#[test] +fn test_decode_auth_switch_cleartext_disabled() { + const AUTH_SWITCH_CLEARTEXT: &[u8] = b"\xfemysql_clear_password\x00abcdefghijabcdefghij\x00"; + + let e = AuthSwitchRequest::decode_with(AUTH_SWITCH_CLEARTEXT.into(), false).unwrap_err(); + + assert_eq!( + e.to_string(), + "encountered unexpected or invalid data: mysql_cleartext_plugin disabled" + ); +} + +#[test] +fn test_decode_auth_switch_packet_no_data() { + const AUTH_SWITCH_NO_DATA: &[u8] = b"\xfemysql_clear_password\x00"; + + let p = AuthSwitchRequest::decode_with(AUTH_SWITCH_NO_DATA.into(), true).unwrap(); + + assert!(matches!(p.plugin, AuthPlugin::MySqlClearPassword)); + assert_eq!(p.data, Bytes::new()); +}