Skip to content

Commit

Permalink
Implement mysql_clear_password (#2533)
Browse files Browse the repository at this point in the history
* mysql_clear_password

* comment

* refactor no-data edge case and add unit tests

* add a connection option to explicitly enable mysql_clear_password

* cargo fmt

* scary comment & log warning
  • Loading branch information
ldanilek committed Jul 31, 2023
1 parent 7e7dded commit febf9ed
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 4 deletions.
6 changes: 6 additions & 0 deletions sqlx-mysql/src/connection/auth.rs
Expand Up @@ -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)
}
}
}

Expand Down
14 changes: 12 additions & 2 deletions sqlx-mysql/src/connection/establish.rs
Expand Up @@ -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<Self, Error> {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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());
Expand Down
17 changes: 17 additions & 0 deletions sqlx-mysql/src/options/mod.rs
Expand Up @@ -76,6 +76,7 @@ pub struct MySqlConnectOptions {
pub(crate) collation: Option<String>,
pub(crate) log_settings: LogSettings,
pub(crate) pipes_as_concat: bool,
pub(crate) enable_cleartext_plugin: bool,
}

impl Default for MySqlConnectOptions {
Expand Down Expand Up @@ -103,6 +104,7 @@ impl MySqlConnectOptions {
statement_cache_capacity: 100,
log_settings: Default::default(),
pipes_as_concat: true,
enable_cleartext_plugin: false,
}
}

Expand Down Expand Up @@ -266,4 +268,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
}
}
3 changes: 3 additions & 0 deletions sqlx-mysql/src/protocol/auth.rs
Expand Up @@ -7,6 +7,7 @@ pub enum AuthPlugin {
MySqlNativePassword,
CachingSha2Password,
Sha256Password,
MySqlClearPassword,
}

impl AuthPlugin {
Expand All @@ -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",
}
}
}
Expand All @@ -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)),
}
Expand Down
51 changes: 49 additions & 2 deletions sqlx-mysql/src/protocol/connect/auth_switch.rs
Expand Up @@ -14,8 +14,8 @@ pub struct AuthSwitchRequest {
pub data: Bytes,
}

impl Decode<'_> for AuthSwitchRequest {
fn decode_with(mut buf: Bytes, _: ()) -> Result<Self, Error> {
impl Decode<'_, bool> for AuthSwitchRequest {
fn decode_with(mut buf: Bytes, enable_cleartext_plugin: bool) -> Result<Self, Error> {
let header = buf.get_u8();
if header != 0xfe {
return Err(err_protocol!(
Expand All @@ -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!(
Expand All @@ -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());
}

0 comments on commit febf9ed

Please sign in to comment.