Bind userauth username to the first request

pull/1051/head
Yosuke Shimizu 2026-06-26 15:01:13 +09:00 committed by Aidan Garske
parent f8c43e17ae
commit a2a78da28a
3 changed files with 286 additions and 6 deletions

View File

@ -9561,6 +9561,25 @@ static int DoUserAuthRequest(WOLFSSH* ssh,
}
}
if (ret == WS_SUCCESS && serviceValid) {
/* Bind the username to the first userauth request. A later request
* that changes it ends the session, so a pipelined request cannot
* rebind a pending keyboard-interactive exchange to another user. */
if (!ssh->userAuthSeen) {
ret = wolfSSH_SetUsernameRaw(ssh,
authData.username, authData.usernameSz);
if (ret == WS_SUCCESS)
ssh->userAuthSeen = 1;
}
else if (authData.usernameSz != ssh->userNameSz
|| WMEMCMP(authData.username, ssh->userName,
authData.usernameSz) != 0) {
WLOG(WS_LOG_DEBUG, "DUAR: username change not allowed");
(void)SendDisconnect(ssh, WOLFSSH_DISCONNECT_PROTOCOL_ERROR);
ret = WS_INVALID_STATE_E;
}
}
if (ret == WS_SUCCESS && serviceValid) {
authNameId = NameToId((const char*)authData.authName, authData.authNameSz);
ssh->authId = authNameId;
@ -9592,12 +9611,6 @@ static int DoUserAuthRequest(WOLFSSH* ssh,
begin = len;
}
if (ret == WS_SUCCESS) {
/* Set the username for valid service only */
ret = wolfSSH_SetUsernameRaw(ssh,
authData.username, authData.usernameSz);
}
*idx = begin;
}

View File

@ -69,6 +69,10 @@
#define AssertNotNull(x) Assert((x), ("%s is not null", #x), (#x " => NULL"))
#define AssertIntEQ(x, y) do { int _x = (int)(x); int _y = (int)(y); \
Assert(_x == _y, ("%s == %s", #x, #y), ("%d != %d", _x, _y)); } while (0)
#define AssertStrEQ(x, y) do { const char* _x = (const char*)(x); \
const char* _y = (const char*)(y); \
Assert(WSTRCMP(_x, _y) == 0, ("%s == %s", #x, #y), \
("\"%s\" != \"%s\"", _x, _y)); } while (0)
static void ResetSession(WOLFSSH* ssh)
@ -1602,6 +1606,264 @@ static void TestSecondSessionChannelRejected(void)
FreeChannelOpenHarness(&harness);
}
/* Records the username the auth callback was invoked with, so a test can prove
* which identity a userauth request bound to. */
static char authCbUserName[64];
static word32 authCbUserNameSz;
static int authCbInvoked;
static void ResetAuthCbRecord(void)
{
WMEMSET(authCbUserName, 0, sizeof(authCbUserName));
authCbUserNameSz = 0;
authCbInvoked = 0;
}
static int RecordUserAuthCb(byte authType, WS_UserAuthData* authData, void* ctx)
{
(void)ctx;
#ifndef WOLFSSH_KEYBOARD_INTERACTIVE
(void)authType;
#endif
#ifdef WOLFSSH_KEYBOARD_INTERACTIVE
if (authType == WOLFSSH_USERAUTH_KEYBOARD_SETUP) {
static byte kbPrompt[] = "Password: ";
static byte* kbPrompts[1];
static word32 kbPromptLengths[1];
static byte kbPromptEcho[1];
kbPrompts[0] = kbPrompt;
kbPromptLengths[0] = (word32)sizeof(kbPrompt) - 1;
kbPromptEcho[0] = 0;
authData->sf.keyboard.promptCount = 1;
authData->sf.keyboard.prompts = kbPrompts;
authData->sf.keyboard.promptLengths = kbPromptLengths;
authData->sf.keyboard.promptEcho = kbPromptEcho;
authData->sf.keyboard.promptName = NULL;
authData->sf.keyboard.promptInstruction = NULL;
authData->sf.keyboard.promptLanguage = NULL;
return WOLFSSH_USERAUTH_SUCCESS;
}
#endif
authCbInvoked = 1;
authCbUserNameSz = authData->usernameSz;
if (authCbUserNameSz >= (word32)sizeof(authCbUserName))
authCbUserNameSz = (word32)sizeof(authCbUserName) - 1;
if (authData->username != NULL && authCbUserNameSz > 0)
WMEMCPY(authCbUserName, authData->username, authCbUserNameSz);
authCbUserName[authCbUserNameSz] = '\0';
#ifdef WOLFSSH_KEYBOARD_INTERACTIVE
if (authType == WOLFSSH_USERAUTH_KEYBOARD)
return WOLFSSH_USERAUTH_SUCCESS;
#endif
/* Reject other methods so the connection stays up for the next request
* without completing authentication. */
return WOLFSSH_USERAUTH_INVALID_PASSWORD;
}
static word32 BuildUserAuthPasswordRequest(const char* user, const char* pw,
byte* out, word32 outSz)
{
byte payload[256];
word32 idx = 0;
idx = AppendString(payload, sizeof(payload), idx, user);
idx = AppendString(payload, sizeof(payload), idx, "ssh-connection");
idx = AppendString(payload, sizeof(payload), idx, "password");
idx = AppendByte(payload, sizeof(payload), idx, 0); /* not changing pw */
idx = AppendString(payload, sizeof(payload), idx, pw);
return WrapPacket(MSGID_USERAUTH_REQUEST, payload, idx, out, outSz);
}
/* Drive a userauth request against a fresh in-memory server harness. The accept
* state is rewound to before userauth completes so USERAUTH_REQUEST and
* USERAUTH_INFO_RESPONSE are accepted by the message filter. */
static void InitUserAuthHarness(ChannelOpenHarness* harness,
byte* in, word32 inSz)
{
InitChannelOpenHarness(harness, in, inSz);
wolfSSH_SetUserAuth(harness->ctx, RecordUserAuthCb);
harness->ssh->acceptState = ACCEPT_SERVER_USERAUTH_ACCEPT_SENT;
}
static void RepointHarnessInput(ChannelOpenHarness* harness,
byte* in, word32 inSz)
{
harness->io.in = in;
harness->io.inSz = inSz;
harness->io.inOff = 0;
harness->io.outSz = 0;
}
/* A username change after the first userauth request must end the session. */
static void TestUsernameChangeDisconnects(void)
{
ChannelOpenHarness harness;
byte inAlice[128];
byte inBob[128];
word32 inAliceSz;
word32 inBobSz;
inAliceSz = BuildUserAuthPasswordRequest("alice", "pw",
inAlice, sizeof(inAlice));
inBobSz = BuildUserAuthPasswordRequest("bob", "pw", inBob, sizeof(inBob));
ResetAuthCbRecord();
InitUserAuthHarness(&harness, inAlice, inAliceSz);
/* First request binds the username; callback sees alice and rejects. */
AssertIntEQ(DoReceive(harness.ssh), WS_SUCCESS);
AssertStrEQ(authCbUserName, "alice");
/* Second request changes the username: session must be torn down and the
* callback must not be invoked as bob. */
ResetAuthCbRecord();
RepointHarnessInput(&harness, inBob, inBobSz);
AssertIntEQ(DoReceive(harness.ssh), WS_FATAL_ERROR);
AssertIntEQ(harness.ssh->error, WS_INVALID_STATE_E);
AssertTrue(harness.io.outSz > 0);
AssertIntEQ(ParseMsgId(harness.io.out, harness.io.outSz), MSGID_DISCONNECT);
AssertIntEQ(authCbInvoked, 0);
FreeChannelOpenHarness(&harness);
}
/* A second userauth request that keeps the username is the normal retry path
* (failed password, or publickey probe then password) and must be accepted. */
static void TestSameUserRetryAllowed(void)
{
ChannelOpenHarness harness;
byte inFirst[128];
byte inSecond[128];
word32 inFirstSz;
word32 inSecondSz;
inFirstSz = BuildUserAuthPasswordRequest("alice", "wrong",
inFirst, sizeof(inFirst));
inSecondSz = BuildUserAuthPasswordRequest("alice", "pw",
inSecond, sizeof(inSecond));
ResetAuthCbRecord();
InitUserAuthHarness(&harness, inFirst, inFirstSz);
/* First request binds alice; the callback rejects the password. */
AssertIntEQ(DoReceive(harness.ssh), WS_SUCCESS);
AssertIntEQ(authCbInvoked, 1);
AssertStrEQ(authCbUserName, "alice");
AssertIntEQ(ParseMsgId(harness.io.out, harness.io.outSz),
MSGID_USERAUTH_FAILURE);
/* Retry as the same user: accepted, callback runs again bound to alice. */
ResetAuthCbRecord();
RepointHarnessInput(&harness, inSecond, inSecondSz);
AssertIntEQ(DoReceive(harness.ssh), WS_SUCCESS);
AssertIntEQ(harness.ssh->error, WS_SUCCESS);
AssertIntEQ(authCbInvoked, 1);
AssertStrEQ(authCbUserName, "alice");
AssertTrue(harness.io.outSz > 0);
AssertIntEQ(ParseMsgId(harness.io.out, harness.io.outSz),
MSGID_USERAUTH_FAILURE);
FreeChannelOpenHarness(&harness);
}
#ifdef WOLFSSH_KEYBOARD_INTERACTIVE
static word32 BuildUserAuthKeyboardRequest(const char* user,
byte* out, word32 outSz)
{
byte payload[256];
word32 idx = 0;
idx = AppendString(payload, sizeof(payload), idx, user);
idx = AppendString(payload, sizeof(payload), idx, "ssh-connection");
idx = AppendString(payload, sizeof(payload), idx, "keyboard-interactive");
idx = AppendString(payload, sizeof(payload), idx, ""); /* language tag */
idx = AppendString(payload, sizeof(payload), idx, ""); /* submethods */
return WrapPacket(MSGID_USERAUTH_REQUEST, payload, idx, out, outSz);
}
static word32 BuildUserAuthInfoResponse(const char* response,
byte* out, word32 outSz)
{
byte payload[128];
word32 idx = 0;
idx = AppendUint32(payload, sizeof(payload), idx, 1); /* responseCount */
idx = AppendString(payload, sizeof(payload), idx, response);
return WrapPacket(MSGID_USERAUTH_INFO_RESPONSE, payload, idx, out, outSz);
}
/* The reported attack: pipeline a keyboard-interactive request for alice then
* bob before answering. The bob request must end the session, not rebind the
* pending challenge. */
static void TestKbUsernameChangeDisconnects(void)
{
ChannelOpenHarness harness;
byte inAlice[128];
byte inBob[128];
word32 inAliceSz;
word32 inBobSz;
inAliceSz = BuildUserAuthKeyboardRequest("alice", inAlice, sizeof(inAlice));
inBobSz = BuildUserAuthKeyboardRequest("bob", inBob, sizeof(inBob));
ResetAuthCbRecord();
InitUserAuthHarness(&harness, inAlice, inAliceSz);
/* alice's request opens the challenge: server emits an INFO_REQUEST. */
AssertIntEQ(DoReceive(harness.ssh), WS_SUCCESS);
AssertTrue(harness.io.outSz > 0);
AssertIntEQ(ParseMsgId(harness.io.out, harness.io.outSz),
MSGID_USERAUTH_INFO_REQUEST);
AssertIntEQ(authCbInvoked, 0); /* setup only, no response yet */
/* bob's pipelined request must disconnect, never reach the callback. */
ResetAuthCbRecord();
RepointHarnessInput(&harness, inBob, inBobSz);
AssertIntEQ(DoReceive(harness.ssh), WS_FATAL_ERROR);
AssertIntEQ(harness.ssh->error, WS_INVALID_STATE_E);
AssertTrue(harness.io.outSz > 0);
AssertIntEQ(ParseMsgId(harness.io.out, harness.io.outSz), MSGID_DISCONNECT);
AssertIntEQ(authCbInvoked, 0);
FreeChannelOpenHarness(&harness);
}
/* A single-user keyboard-interactive exchange still reaches the callback bound
* to the originating user. */
static void TestKbSameUserResponseSucceeds(void)
{
ChannelOpenHarness harness;
byte inReq[128];
byte inResp[128];
word32 inReqSz;
word32 inRespSz;
inReqSz = BuildUserAuthKeyboardRequest("alice", inReq, sizeof(inReq));
inRespSz = BuildUserAuthInfoResponse("secret", inResp, sizeof(inResp));
ResetAuthCbRecord();
InitUserAuthHarness(&harness, inReq, inReqSz);
AssertIntEQ(DoReceive(harness.ssh), WS_SUCCESS);
AssertIntEQ(ParseMsgId(harness.io.out, harness.io.outSz),
MSGID_USERAUTH_INFO_REQUEST);
RepointHarnessInput(&harness, inResp, inRespSz);
AssertIntEQ(DoReceive(harness.ssh), WS_SUCCESS);
AssertIntEQ(authCbInvoked, 1);
AssertStrEQ(authCbUserName, "alice");
FreeChannelOpenHarness(&harness);
}
#endif /* WOLFSSH_KEYBOARD_INTERACTIVE */
#ifdef WOLFSSH_FWD
static void TestDirectTcpipRejectSendsOpenFail(void)
{
@ -4637,6 +4899,8 @@ int main(int argc, char** argv)
#endif
TestChannelOpenCallbackRejectSendsOpenFail();
TestSecondSessionChannelRejected();
TestUsernameChangeDisconnects();
TestSameUserRetryAllowed();
#ifdef WOLFSSH_FWD
TestDirectTcpipRejectSendsOpenFail();
TestDirectTcpipNoFwdCbSendsOpenFail();
@ -4723,6 +4987,8 @@ int main(int argc, char** argv)
TestKeyboardResponseNoUserAuthCallback(ssh, ctx);
TestKeyboardResponseNullSsh();
TestKeyboardResponseNullCtx(ssh);
TestKbUsernameChangeDisconnects();
TestKbSameUserResponseSucceeds();
#endif
/* TODO: add app-level regressions that simulate stdin EOF/password

View File

@ -1041,6 +1041,7 @@ struct WOLFSSH {
word32 testSftpSendCap; /* test hook: cap per-call SFTP buffer send */
word32 testSftpStallPending; /* test hook: force N flush-only resumes */
#endif
byte userAuthSeen; /* a userauth request has bound the username */
};