Implement mysql_clear_password (#2533)

* 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
This commit is contained in:
Lee Danilek 2023-07-31 15:41:02 -04:00 committed by GitHub
parent 7e7dded8af
commit febf9ed775
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 87 additions and 4 deletions

View File

@ -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)
}
}
}

View File

@ -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> {
@ -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());

View File

@ -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 {
@ -103,6 +104,7 @@ impl MySqlConnectOptions {
statement_cache_capacity: 100,
log_settings: Default::default(),
pipes_as_concat: true,
enable_cleartext_plugin: false,
}
}
@ -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
}
}

View File

@ -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)),
}

View File

@ -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!(
@ -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());
}