process_pkt_COM_CHANGE_USER unconditionally copies attributes from
the lookup-result account_details onto the live session BEFORE
authentication is verified:
sess->default_hostgroup = account_details.default_hostgroup;
sess->transaction_persistent = account_details.transaction_persistent;
free(sess->user_attributes); sess->user_attributes = account_details.attributes;
The previous version of the pass-through rejection block sat AFTER
those mutations. For a CHANGE_USER targeting a pass-through-eligible
user that we then reject, the rejected attempt had a side effect: the
session's already-authenticated state was now grafted with the
target row's routing / transaction semantics / user_attributes.
That's an observable side-effect from a rejected attempt -- benign
when the upstream fatal-closes on ret=false, but a meaningful
security property when the session continues. The cleaner fix is to
do the rejection check BEFORE the mutations: we obtain a temporary
early_password via get_password(), evaluate the gate, and either
return out with the session untouched or fall through to the legacy
path. The temporary buffer is explicitly freed in both branches.
The existing legacy failure paths (password==NULL, etc.) still
exhibit the same pre-mutation property since they too were
authored as authentication-first / state-mutation-first. That's
project-wide and out of scope here; this commit only ensures the
new pass-through rejection doesn't make it WORSE.
Discovered by the new-code-quality subagent during the second
deep review of PR #5810.