Skip to content

[RFC violation] Client accepts an unsolicited SSH_MSG_USERAUTH_SUCCESS and reports success to the application #1285

Description

@LCBH

[RFC violation] Client accepts an unsolicited SSH_MSG_USERAUTH_SUCCESS and reports success to the application

Component: client state machine — src/internal.c (IsMessageAllowedClient, DoUserAuthSuccess), src/ssh.c (wolfSSH_connect)
Affected versions: v1.5.0-stable; master 6e49506b (2026-09-18) — both. Re-verified present at 6ed8e121 (2026-09-25), master HEAD at the time of writing
Type: RFC 4252 conformance / client state machine
Severity: Low (see Impact)
Discovery method: differential fuzzing with tlspuffin, then reduced to the standalone PoC below


Summary

A wolfSSH client that sends SSH_MSG_SERVICE_REQUEST and receives SSH_MSG_USERAUTH_SUCCESS (52) in
reply — rather than SSH_MSG_SERVICE_ACCEPT (6) — treats itself as authenticated and proceeds to the
connection layer, without ever receiving a reply to its own SSH_MSG_USERAUTH_REQUEST.

wolfSSH_connect() returns success to the calling application when no authentication has occurred, so an
application built on it mis-states its own security state. Of the conformance issues we are reporting, this
is the one we would fix first.

RFC citation

RFC 4252 §5.1, on SSH_MSG_USERAUTH_SUCCESS:

The server MUST NOT send this message until authentication has been successfully completed.

and RFC 4253 §10, on the service request:

After the key exchange, the client requests a service. […] If the server supports the service (and
permits the client to use it), it MUST respond with SSH_MSG_SERVICE_ACCEPT.

A client is entitled to expect that a USERAUTH_SUCCESS corresponds to an authentication request it made.
wolfSSH accepts one that answers nothing.

Reproduction

Server side is a ~190-line Python script speaking SSH-2.0 directly (curve25519-sha256 +
aes256-gcm@openssh.com, RSA host key signing rsa-sha2-256 over H, no SSH library). It completes an
honest key exchange, answers the client's SERVICE_REQUEST with 52 instead of 6, and then
deliberately never answers the client's own USERAUTH_REQUEST.

The non-answer is what makes the result conclusive: if the client reaches the connection layer having had no
reply to its authentication request, the only thing it can have acted on is the unsolicited 52.

Observed message sequence, identical on v1.5.0-stable and master:

client -> server   5   SERVICE_REQUEST  ("ssh-userauth")
server -> client  52   USERAUTH_SUCCESS          <-- unsolicited; no SERVICE_ACCEPT sent
client -> server  50   USERAUTH_REQUEST
                       (server sends NO reply to this, deliberately)
client -> server  90   CHANNEL_OPEN     ("session")
client -> server  98   CHANNEL_REQUEST  ("shell")

wolfSSH_connect() returns WS_SUCCESS. The client proceeds from an unsolicited USERAUTH_SUCCESS to
requesting an interactive shell, unaided by any application code.

Control: the same client against the same server in honest mode (SERVICE_ACCEPT, then USERAUTH_SUCCESS
only after a real USERAUTH_REQUEST) behaves correctly.

The client under test is a minimal wolfSSH_connect program using the public API only
(wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT) / SetUsername / set_fd / connect). It deliberately does not
call wolfSSH_worker: the finding is complete at the end of wolfSSH_connect, and driving application
traffic would test the application rather than wolfSSH.

Attached reproducer — one file, runs both modes, prints a verdict

repro_issue_1_unsolicited_userauth_success.py (attached) is self-contained: it embeds the ~40-line
client, compiles it against your wolfSSH build, generates its own host key, runs both modes and reports.
Requires python3 with cryptography.

$ python3 repro_issue_1_unsolicited_userauth_success.py --wolfssh /path/to/wolfssh

  mode         connect  auth reply?  reached
  unsolicited  SUCCESS  none         CHANNEL_OPEN(session), CHANNEL_REQUEST(shell)
  honest       SUCCESS  yes          CHANNEL_OPEN(session), CHANNEL_REQUEST(shell)

  VERDICT: REPRODUCED -- the client reached the connection layer with NO reply
           to its USERAUTH_REQUEST, so it acted on the unsolicited 52.

It exits 0 when reproduced and 1 when not, so it can be dropped into a regression run. --client PATH uses
a client you have already built instead of compiling the embedded one. The output above is a real run
against v1.5.0-stable.

Root cause

Two independent conditions combine.

1. The userauth gate opens before any authentication request exists. IsMessageAllowedClient gates the
userauth message range on connectState < CONNECT_CLIENT_USERAUTH_REQUEST_SENT:

else if (MSGIDLIMIT_AUTH(msg)) {
    /* Do not accept any userauth messages until we've asked for auth. */
    if (ssh->connectState < CONNECT_CLIENT_USERAUTH_REQUEST_SENT) { ...reject... }
    return 1;
}

but that state is set immediately after SendServiceRequest — ssh.c:845 sends the service request,
ssh.c:850 sets connectState = CONNECT_CLIENT_USERAUTH_REQUEST_SENT. So the gate is already satisfied
when the SERVICE_REQUEST goes out, and the comment's intent ("until we've asked for auth") is not what the
code tests.

2. DoUserAuthSuccess validates nothing. Its only check is that the payload is empty:

static int DoUserAuthSuccess(WOLFSSH* ssh, byte* buf, word32 len, word32* idx)
{
    /* This message does not have any payload. len should be 0. */
    if (ssh == NULL || buf == NULL || len != 0 || idx == NULL) { ret = WS_BAD_ARGUMENT; ... }
    ssh->serverState = SERVER_USERAUTH_ACCEPT_DONE;
    ...
}

Why the conclusion is closed rather than behavioural. CHANNEL_OPEN is emitted only in the
CONNECT_SERVER_USERAUTH_ACCEPT_DONE arm of wolfSSH_connect, which is reached only when

case CONNECT_CLIENT_USERAUTH_SENT:
    while (ssh->serverState < SERVER_USERAUTH_ACCEPT_DONE) {
        if (DoReceive(ssh) < WS_SUCCESS) { ... return WS_FATAL_ERROR; }
    }

exits — ssh.c:888 in v1.5.0, a loop with no timeout, no retry-to-advance and no alternative exit: the only
way past it is for serverState to reach SERVER_USERAUTH_ACCEPT_DONE. And
serverState = SERVER_USERAUTH_ACCEPT_DONE is assigned at exactly one site in each tree: inside
DoUserAuthSuccess (v1.5.0 internal.c:8495, master internal.c:11927), reachable only on receipt of
message 52. A client that sends CHANNEL_OPEN having received no reply to its USERAUTH_REQUEST must
therefore have processed an unsolicited 52.

Suggested fix

Track whether a USERAUTH_REQUEST was actually sent, rather than inferring it from an ordered connectState
comparison that the service request already satisfies:

/* in the userauth send path */
ssh->userAuthRequestSent = 1;

/* in IsMessageAllowedClient */
else if (MSGIDLIMIT_AUTH(msg)) {
    if (!ssh->userAuthRequestSent) { ...reject... }
    return 1;
}

Optionally, have DoUserAuthSuccess refuse when no request is outstanding, so the check does not depend on
the filter alone.

Impact

Low. The message must arrive over a completed key exchange, so the sender is one the client has already
accepted by host key — the legitimate server, or a holder of its private key. Such a party can already
induce the identical client state by answering the authentication request with USERAUTH_SUCCESS
, so the
defect grants no capability that party does not have; it only saves it waiting for the request.

What makes it worth fixing is not exploitability but that it is the one issue here that mis-states the
security state of the connection to the application above it
: wolfSSH_connect() returns success where no
authentication occurred, and an application has no way to tell the difference.

Notes

A sibling shape exists where the trigger is an unsolicited USERAUTH_FAILURE before any SERVICE_ACCEPT —
the same gate, a milder consequence. The fix above covers both.

We verified this is present on master before reporting, specifically to avoid reporting something already
fixed. IsMessageAllowedClient's userauth arm and DoUserAuthSuccess are unchanged there in the respects
that matter.

Acknowledgements

Found with the tlspuffin fuzzer — https://github.com/tlspuffin/tlspuffin — designed and developed by the
tlspuffin team: Nataël Baffou, Olivier Demengeon, Tom Gouville, Lucca Hirschi, Steve Kremer (Inria, LORIA, France).

repro_issue_1_unsolicited_userauth_success.py

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions