fix(mysqlx): enforce per-user require_tls and allowed_auth_methods

Two MysqlxResolvedIdentity fields were loaded from runtime_mysqlx_users
into the in-memory store but never consulted by the auth path:

  identity_->require_tls           // per-user "must be over TLS"
  identity_->allowed_auth_methods  // per-user mechanism whitelist

Net effect:

  - require_tls=1 did not reject MYSQL41-over-plaintext. Operators
    setting this column expected a hardening guarantee they did not
    actually get; an attacker bypassing TLS could still authenticate
    via MYSQL41 (which is challenge-response over plaintext, but
    leaks the user's full SHA1(SHA1(pw)) on a passive eavesdrop and
    is replayable for the duration of the auth_challenge_).

  - allowed_auth_methods='MYSQL41' did not reject PLAIN attempts
    (and vice versa). Operators expected to be able to lock down a
    user to a specific mechanism; the column was decorative.

# Fix

New helper MysqlxSession::enforce_identity_policy() called from both
auth resolution sites (handle_auth_plain at line ~539, handler_auth_
challenge_sent at line ~664) immediately after identity_ resolves
and before credential verification. It:

  - rejects with 1045 "User requires a TLS connection" when
    identity_->require_tls && !client_ds_.is_encrypted().
  - rejects with 1045 "Authentication mechanism not allowed for
    user" when identity_->allowed_auth_methods is non-empty and
    auth_method_ is not in the comma-separated list (case-
    insensitive, whitespace-trimmed).

Empty allowed_auth_methods preserves the historical "any wired
method" default so existing rows don't require a backfill — the
column already defaulted to '' in the table DDL.

# What this commit does NOT cover

backend_auth_mode is the third field in the same review finding
(reviewer's #2). It's partially implemented today by side-effect:
the backend_username being non-empty selects the service_account
codepath, empty selects mapped. The pass_through mode would require
forwarding the frontend AuthStart frame unmodified to the backend
and is not implemented; it remains TBD pending issue #5693
(asymmetric TLS / AsClient) which has overlapping protocol-shape
concerns. This commit deliberately scopes to the two policy checks
that are unambiguously broken; backend_auth_mode pass_through gets
its own commit when #5693 lands.

# Verified

21 / 21 mysqlx unit tests still green. Caught by an external review
pass, finding #2.
pull/5702/head
Rene Cannao 2 months ago
parent 4e32f44196
commit e535a66ee1

@ -177,6 +177,20 @@ private:
void handle_auth_mysql41(const std::string& auth_data);
void handle_auth_plain(const std::string& auth_data);
// After identity_ is resolved, validate that the user is allowed to
// authenticate with the negotiated mechanism over the current
// transport. Sends the appropriate X-Protocol error frame and
// returns false if any per-identity policy is violated:
// - identity_->require_tls=1 and the frontend connection is
// not encrypted
// - identity_->allowed_auth_methods is set and does not contain
// auth_method_ (a comma-separated list; empty means
// "any wired method allowed", matching the historical default)
// Caller is expected to set `healthy = false` and return on a
// false return value (the helper itself does not change session
// state beyond emitting the error frame).
bool enforce_identity_policy();
void forward_frame_to_client(uint8_t msg_type, const MysqlxFrame& frame);
int dispatch_client_message(uint8_t msg_type);

@ -474,6 +474,51 @@ void MysqlxSession::handler_capabilities_set() {
status_ = CONNECTING_CLIENT;
}
bool MysqlxSession::enforce_identity_policy() {
if (!identity_) {
return true; // nothing to enforce
}
// require_tls: per-user "MYSQL41 / PLAIN must run over TLS".
// PLAIN already has a hardcoded TLS gate at handle_auth_plain entry;
// this is the per-user knob that also covers MYSQL41 — operators set
// require_tls=1 on a row to forbid the (otherwise legal) "MYSQL41
// over plaintext" path.
if (identity_->require_tls && !client_ds_.is_encrypted()) {
send_error(1045, "User requires a TLS connection");
return false;
}
// allowed_auth_methods: per-user whitelist of mechanism names.
// Empty string preserves the historical "any wired method" default
// so existing rows don't require a backfill. Non-empty: comma-
// separated, case-insensitive match against auth_method_.
if (!identity_->allowed_auth_methods.empty()) {
const std::string& list = identity_->allowed_auth_methods;
bool matched = false;
size_t pos = 0;
while (pos < list.size()) {
size_t comma = list.find(',', pos);
if (comma == std::string::npos) comma = list.size();
std::string token = list.substr(pos, comma - pos);
// Trim leading/trailing whitespace.
while (!token.empty() && (token.front() == ' ' || token.front() == '\t')) token.erase(token.begin());
while (!token.empty() && (token.back() == ' ' || token.back() == '\t')) token.pop_back();
if (!token.empty() && strcasecmp(token.c_str(), auth_method_.c_str()) == 0) {
matched = true;
break;
}
pos = comma + 1;
}
if (!matched) {
send_error(1045, "Authentication mechanism not allowed for user");
return false;
}
}
return true;
}
void MysqlxSession::handle_auth_mysql41(const std::string& auth_data) {
size_t first_nul = auth_data.find('\0');
if (first_nul != std::string::npos) {
@ -535,6 +580,11 @@ void MysqlxSession::handle_auth_plain(const std::string& auth_data) {
return;
}
if (!enforce_identity_policy()) {
healthy = false;
return;
}
std::vector<uint8_t> stored_hash;
if (!derive_stored_hash(identity_->password, stored_hash)) {
send_error(1045, "Access denied for user");
@ -663,6 +713,11 @@ void MysqlxSession::handler_auth_challenge_response() {
return;
}
if (!enforce_identity_policy()) {
healthy = false;
return;
}
std::vector<uint8_t> stored_hash;
if (!derive_stored_hash(identity_->password, stored_hash)) {
send_error(1045, "Access denied for user");

Loading…
Cancel
Save