fix(CLI-40): scrub the credential from the redacted error's structured reply, route MXACCESS_FAILURE to MxAccess (Rust), fix Go Subscribe terminal-error drop
Code-review follow-up on the CLI-40/41/44 branch. ISSUE 1 (all five, critical): the message-only scrub still leaked the server-echoed credential through the redacted error's structured reply accessor (.NET Reply/Statuses, Java reply()/protocolStatus(), Go MxAccessError.Reply via errors.As, Rust reply()/into_reply(), Python raw_reply). The redacted error now carries a scrubbed clone of the reply (protocol_status.message, diagnostic_message, statuses[].diagnostic_text), with per-language tests asserting the reply accessor no longer contains the credential. ISSUE 2 (Rust, critical): ensure_command_success routed MXACCESS_FAILURE to Error::Command (unlike the other four clients), bypassing attach_secrets and leaking via derived Debug/Display. MXACCESS_FAILURE now routes to Error::MxAccess, fixing the cross-client inconsistency. ISSUE 3 (Go, important): the CLI-44 terminal send was unconditionally non-blocking, dropping a genuine terminal error under a full buffer on the never-drop SubscribeEvents path. It is now reserved-slot-non-blocking only for the cancel-on-overflow path and blocking for the never-drop path. New shared fixture authenticate-user.echoed-credential-mxaccess-failure.reply.json wired into all five suites. Minors: whitespace-secret guard on .NET/Java redact helpers; Java preserves exception subtype on redaction; redaction-helper unit tests (Go/Java/.NET). Docs (ClientBehaviorFixtures.md, ClientLibrariesDesign.md) updated to make the structured-field claim true.
This commit is contained in:
@@ -127,3 +127,71 @@ func TestAuthenticateUserScrubsEchoedCredentialFromError(t *testing.T) {
|
||||
t.Fatalf("surfaced error missing redaction marker: %q", message)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAuthenticateUserScrubsEchoedCredentialFromStructuredReply is the CLI-40
|
||||
// follow-up: redacting only the rendered Error() string is not enough. The typed
|
||||
// *MxAccessError still carries the raw command reply, whose ProtocolStatus.Message,
|
||||
// DiagnosticMessage, and Statuses[].DiagnosticText echo the credential verbatim. A
|
||||
// logger dumping structured fields would reintroduce the leak, so the reply the
|
||||
// typed error carries must be a scrubbed clone. Both the OK+negative-HRESULT and the
|
||||
// MXACCESS_FAILURE fixtures route to *MxAccessError (via EnsureProtocolSuccess), so
|
||||
// both must be scrubbed identically.
|
||||
func TestAuthenticateUserScrubsEchoedCredentialFromStructuredReply(t *testing.T) {
|
||||
const credential = "sup3rSecretVerify9f3a2b"
|
||||
fixtures := []string{
|
||||
"authenticate-user.echoed-credential.reply.json",
|
||||
"authenticate-user.echoed-credential-mxaccess-failure.reply.json",
|
||||
}
|
||||
for _, fixture := range fixtures {
|
||||
t.Run(fixture, func(t *testing.T) {
|
||||
fake := &fakeGatewayServer{
|
||||
invokeReply: loadCommandReplyFixture(t, fixture),
|
||||
}
|
||||
client, cleanup := newBufconnClient(t, fake)
|
||||
defer cleanup()
|
||||
session := NewSessionForID(client, "session-1")
|
||||
|
||||
_, err := session.AuthenticateUser(context.Background(), 12, "operator", credential)
|
||||
if err == nil {
|
||||
t.Fatal("AuthenticateUser() error = nil, want an MXAccess failure")
|
||||
}
|
||||
|
||||
var mxErr *MxAccessError
|
||||
if !errors.As(err, &mxErr) {
|
||||
t.Fatalf("AuthenticateUser() error = %v (%T), want *MxAccessError", err, err)
|
||||
}
|
||||
|
||||
reply := mxErr.Reply
|
||||
if reply == nil {
|
||||
t.Fatal("MxAccessError.Reply is nil, want the scrubbed command reply")
|
||||
}
|
||||
if got := reply.GetProtocolStatus().GetMessage(); strings.Contains(got, credential) {
|
||||
t.Fatalf("MxAccessError.Reply.ProtocolStatus.Message leaked the credential: %q", got)
|
||||
}
|
||||
if got := reply.GetDiagnosticMessage(); strings.Contains(got, credential) {
|
||||
t.Fatalf("MxAccessError.Reply.DiagnosticMessage leaked the credential: %q", got)
|
||||
}
|
||||
for i, status := range reply.GetStatuses() {
|
||||
if got := status.GetDiagnosticText(); strings.Contains(got, credential) {
|
||||
t.Fatalf("MxAccessError.Reply.Statuses[%d].DiagnosticText leaked the credential: %q", i, got)
|
||||
}
|
||||
}
|
||||
|
||||
// The wrapped CommandError's status/reply must be scrubbed too.
|
||||
if mxErr.Command != nil {
|
||||
if got := mxErr.Command.Status.GetMessage(); strings.Contains(got, credential) {
|
||||
t.Fatalf("MxAccessError.Command.Status.Message leaked the credential: %q", got)
|
||||
}
|
||||
if cmdReply := mxErr.Command.Reply; cmdReply != nil {
|
||||
if got := cmdReply.GetDiagnosticMessage(); strings.Contains(got, credential) {
|
||||
t.Fatalf("MxAccessError.Command.Reply.DiagnosticMessage leaked the credential: %q", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if got := err.Error(); strings.Contains(got, credential) {
|
||||
t.Fatalf("rendered error leaked the credential: %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user