From 549c5091037c0fea0350413aae694648e643bedf Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 14 Oct 2026 14:46:32 -0700 Subject: [PATCH 1/2] wolfsshd: hide account existence in userauth DefaultUserAuthTypes() resolved the requested user before any authentication, so a username with no local account produced an empty "methods that can continue" name-list. It now falls back to the global configuration's methods when the user does not resolve, so the USERAUTH_FAILURE reply no longer says which accounts exist. - an unresolved user still cannot authenticate, since the same failed lookup rejects every method it tries - test_configuration covers the advertisement for an unknown user, comparing against the global mask so it tracks the configuration Issue: ZD-22617 --- apps/wolfsshd/auth.c | 4 +- apps/wolfsshd/test/test_configuration.c | 85 +++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index eadb0a027..df1f80c7a 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -3089,7 +3089,9 @@ int DefaultUserAuthTypes(WOLFSSH* ssh, void* ctx) usrConf = wolfSSHD_AuthGetUserConf(authCtx, usr, NULL, NULL, NULL, NULL, NULL); if (usrConf == NULL) { - ret = 0; + /* The user did not resolve. Advertise the global methods anyway: an + * empty name-list would tell the peer the account does not exist. */ + ret = wolfSSHD_GetUserAuthTypes(authCtx->conf); } else { ret = wolfSSHD_GetUserAuthTypes(usrConf); diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index d8dc67f89..4fe59ec6d 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -5705,6 +5705,90 @@ static int test_DefaultUserAuthTypesNullArgs(void) return ret; } +/* A username with no local account must advertise the same methods as a real + * one, so the USERAUTH_FAILURE name-list does not tell an unauthenticated peer + * which accounts exist. Compares the advertisement for a bogus user against + * the global mask, so it is checked to track the configuration rather than a + * fixed list. */ +static int test_DefaultUserAuthTypesUnknownUser(void) +{ + int ret = WS_SUCCESS; + int i; + + static const struct { + const char* desc; + const char* line; /* extra directive, NULL for the stock config */ + } vectors[] = { + {"stock config advertises both methods for an unknown user", NULL}, + {"PasswordAuthentication no is honored for an unknown user", + "PasswordAuthentication no"}, + }; + const int numVectors = (int)(sizeof(vectors) / sizeof(*vectors)); + WOLFSSHD_CONFIG* conf; + WOLFSSHD_AUTH* authCtx; + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + int expected; + /* long enough that a real account by this name is implausible */ + static const char usr[] = "nonexistent_test_user_xyz"; + /* keeps the permission drop in wolfSSHD_AuthCreateUser a no-op; see + * test_DefaultUserAuthPasswordNotTerminated */ + static const char privSep[] = "UsePrivilegeSeparation no"; + + for (i = 0; i < numVectors && ret == WS_SUCCESS; ++i) { + Log(" Testing scenario: %s.", vectors[i].desc); + + conf = wolfSSHD_ConfigNew(NULL); + if (conf == NULL) { + Log(" FAILED.\n"); + return WS_MEMORY_E; + } + + ret = ParseConfigLine(&conf, privSep, (int)WSTRLEN(privSep), 0); + if (ret == WS_SUCCESS && vectors[i].line != NULL) { + ret = ParseConfigLine(&conf, vectors[i].line, + (int)WSTRLEN(vectors[i].line), 0); + } + if (ret != WS_SUCCESS) { + Log(" FAILED.\n"); + wolfSSHD_ConfigFree(conf); + break; + } + + authCtx = wolfSSHD_AuthCreateUser(NULL, conf); + if (authCtx == NULL) { + Log(" Skipping test: wolfSSHD_AuthCreateUser failed (likely missing 'sshd' user).\n"); + wolfSSHD_ConfigFree(conf); + break; + } + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + ssh = (ctx != NULL) ? wolfSSH_new(ctx) : NULL; + if (ssh == NULL || wolfSSH_SetUsername(ssh, usr) != WS_SUCCESS) { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + else { + expected = wolfSSHD_GetUserAuthTypes(conf); + if (expected == 0 || + DefaultUserAuthTypes(ssh, authCtx) != expected) { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + else { + Log(" PASSED.\n"); + } + } + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + wolfSSHD_AuthFreeUser(authCtx); + wolfSSHD_ConfigFree(conf); + } + + return ret; +} + #ifndef _WIN32 /* report a single secure-open scenario; returns WS_SUCCESS when the observed * result matches expectation (wantOk != 0 means expect acceptance) */ @@ -7316,6 +7400,7 @@ const TEST_CASE testCases[] = { TEST_DECL(test_ConfigIncludeMatchChain), TEST_DECL(test_GetUserAuthTypes), TEST_DECL(test_DefaultUserAuthTypesNullArgs), + TEST_DECL(test_DefaultUserAuthTypesUnknownUser), TEST_DECL(test_ConfigSetAuthKeysFile), TEST_DECL(test_ResolveAuthKeysPath), TEST_DECL(test_ConfigFree), From b60a0c99af52c680103317ae7d1d98a886aa93c1 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 14 Oct 2026 15:41:21 -0700 Subject: [PATCH 2/2] wolfsshd: match an unknown user by name An unresolved user took the global methods, so a name a Match User block covers advertised a different list depending on whether the account existed. DefaultUserAuthTypes() now composes the blocks that select on the name alone, with no group list, when the account does not exist. - a Match Group block still cannot apply to a name with no account, so it fails closed to the unmatched methods - any other failure to resolve the user, or to compose its blocks, advertises no methods rather than the global ones, so a Match block that disabled a method is never skipped - test_configuration stubs the account lookup and covers a lookup error, an unreadable group list and a resolved account as well as the Match User and Match Group blocks Issue: ZD-22617 --- apps/wolfsshd/auth.c | 14 ++- apps/wolfsshd/test/test_configuration.c | 147 ++++++++++++++++++------ 2 files changed, 124 insertions(+), 37 deletions(-) diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index df1f80c7a..32ef89ad6 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -3088,10 +3088,18 @@ int DefaultUserAuthTypes(WOLFSSH* ssh, void* ctx) usr = wolfSSH_GetUsername(ssh); usrConf = wolfSSHD_AuthGetUserConf(authCtx, usr, NULL, NULL, NULL, NULL, NULL); + if (usrConf == NULL && usr != NULL && authCtx->checkUserCb != NULL && + authCtx->checkUserCb(usr) == WSSHD_AUTH_FAILURE) { + /* No such account: compose on the name alone, so Match User + * applies and Match Group cannot. Other failures stay closed. */ + usrConf = wolfSSHD_GetUserConf(authCtx->conf, usr, NULL, 0, + NULL, NULL, NULL, NULL, NULL); + } + if (usrConf == NULL) { - /* The user did not resolve. Advertise the global methods anyway: an - * empty name-list would tell the peer the account does not exist. */ - ret = wolfSSHD_GetUserAuthTypes(authCtx->conf); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] No user configuration; advertising no methods"); + ret = 0; } else { ret = wolfSSHD_GetUserAuthTypes(usrConf); diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index 4fe59ec6d..22e62df56 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -20,6 +20,7 @@ #ifndef _WIN32 #include #include + #include #endif #if !defined(_WIN32) && !(defined(__OSX__) || defined(__APPLE__)) #include @@ -5705,90 +5706,168 @@ static int test_DefaultUserAuthTypesNullArgs(void) return ret; } -/* A username with no local account must advertise the same methods as a real - * one, so the USERAUTH_FAILURE name-list does not tell an unauthenticated peer - * which accounts exist. Compares the advertisement for a bogus user against - * the global mask, so it is checked to track the configuration rather than a - * fixed list. */ +/* Probed name; also the Match User target below. */ +#define UNKNOWN_USER "nonexistent_test_user_xyz" + +/* Stubbed account lookup for the probed name, independent of the host */ +enum { + ADV_NO_ACCOUNT, /* getpwnam: no such account */ + ADV_LOOKUP_ERROR, /* getpwnam: lookup error */ + ADV_GROUPS_FAIL, /* account exists, its group list cannot be read */ + ADV_RESOLVED /* account exists with a readable group list */ +}; + +#ifndef _WIN32 +static int s_adv_mode; +static struct passwd s_adv_pw; + +static struct passwd* stub_getpwnam_adv(const char* name) +{ + (void)name; + switch (s_adv_mode) { + case ADV_NO_ACCOUNT: + return NULL; + case ADV_LOOKUP_ERROR: + errno = EIO; + return NULL; + default: + WMEMSET(&s_adv_pw, 0, sizeof(s_adv_pw)); + s_adv_pw.pw_name = (char*)UNKNOWN_USER; + s_adv_pw.pw_uid = 1002; + s_adv_pw.pw_gid = 1002; + s_adv_pw.pw_passwd = (char*)"x"; + return &s_adv_pw; + } +} +#endif + +/* An unknown name advertises what the global directives and Match User + * blocks offer it, so the reply does not reveal which accounts exist. A + * lookup error or unreadable group list advertises nothing. */ static int test_DefaultUserAuthTypesUnknownUser(void) { int ret = WS_SUCCESS; - int i; + int i, j; static const struct { const char* desc; - const char* line; /* extra directive, NULL for the stock config */ + int mode; + const char* lines[3]; /* directives, NULL terminated */ + int expected; } vectors[] = { - {"stock config advertises both methods for an unknown user", NULL}, - {"PasswordAuthentication no is honored for an unknown user", - "PasswordAuthentication no"}, + {"stock config advertises both methods", ADV_NO_ACCOUNT, {NULL}, + WOLFSSH_USERAUTH_PASSWORD | WOLFSSH_USERAUTH_PUBLICKEY}, + {"global PasswordAuthentication no advertises pubkey only", + ADV_NO_ACCOUNT, {"PasswordAuthentication no", NULL}, + WOLFSSH_USERAUTH_PUBLICKEY}, + {"Match User on the probed name applies", ADV_NO_ACCOUNT, + {"Match User " UNKNOWN_USER, "PasswordAuthentication no", NULL}, + WOLFSSH_USERAUTH_PUBLICKEY}, + {"Match User on another name does not apply", ADV_NO_ACCOUNT, + {"Match User someone_else", "PasswordAuthentication no", NULL}, + WOLFSSH_USERAUTH_PASSWORD | WOLFSSH_USERAUTH_PUBLICKEY}, + {"Match Group does not apply to a name with no account", + ADV_NO_ACCOUNT, + {"Match Group somegroup", "PasswordAuthentication no", NULL}, + WOLFSSH_USERAUTH_PASSWORD | WOLFSSH_USERAUTH_PUBLICKEY}, +#ifndef _WIN32 + {"account lookup error advertises nothing", ADV_LOOKUP_ERROR, + {NULL}, 0}, + {"unreadable group list advertises nothing", ADV_GROUPS_FAIL, + {NULL}, 0}, + {"resolved account takes its Match User block", ADV_RESOLVED, + {"Match User " UNKNOWN_USER, "PasswordAuthentication no", NULL}, + WOLFSSH_USERAUTH_PUBLICKEY}, +#endif }; const int numVectors = (int)(sizeof(vectors) / sizeof(*vectors)); + const int maxLines = (int)(sizeof(vectors[0].lines) / + sizeof(*vectors[0].lines)); + WOLFSSHD_CONFIG* head; WOLFSSHD_CONFIG* conf; WOLFSSHD_AUTH* authCtx; WOLFSSH_CTX* ctx; WOLFSSH* ssh; - int expected; - /* long enough that a real account by this name is implausible */ - static const char usr[] = "nonexistent_test_user_xyz"; +#ifndef _WIN32 + struct passwd* (*savedGetpwnam)(const char*); + int (*savedGrouplist)(const char*, WGID_T, WGID_T*, int*); + int (*savedSetgroups)(int, const WGID_T*); +#endif /* keeps the permission drop in wolfSSHD_AuthCreateUser a no-op; see - * test_DefaultUserAuthPasswordNotTerminated */ + * test_DefaultUserAuth_OOBRead */ static const char privSep[] = "UsePrivilegeSeparation no"; for (i = 0; i < numVectors && ret == WS_SUCCESS; ++i) { Log(" Testing scenario: %s.", vectors[i].desc); - conf = wolfSSHD_ConfigNew(NULL); - if (conf == NULL) { + head = wolfSSHD_ConfigNew(NULL); + if (head == NULL) { Log(" FAILED.\n"); return WS_MEMORY_E; } + /* a Match line moves 'conf' to the new block; keep the list head */ + conf = head; + /* goes in before any Match line so it lands in the global section */ ret = ParseConfigLine(&conf, privSep, (int)WSTRLEN(privSep), 0); - if (ret == WS_SUCCESS && vectors[i].line != NULL) { - ret = ParseConfigLine(&conf, vectors[i].line, - (int)WSTRLEN(vectors[i].line), 0); + for (j = 0; ret == WS_SUCCESS && j < maxLines && + vectors[i].lines[j] != NULL; ++j) { + ret = ParseConfigLine(&conf, vectors[i].lines[j], + (int)WSTRLEN(vectors[i].lines[j]), 0); } if (ret != WS_SUCCESS) { Log(" FAILED.\n"); - wolfSSHD_ConfigFree(conf); + wolfSSHD_ConfigFree(head); break; } - authCtx = wolfSSHD_AuthCreateUser(NULL, conf); + authCtx = wolfSSHD_AuthCreateUser(NULL, head); if (authCtx == NULL) { - Log(" Skipping test: wolfSSHD_AuthCreateUser failed (likely missing 'sshd' user).\n"); - wolfSSHD_ConfigFree(conf); + Log(" FAILED.\n"); + wolfSSHD_ConfigFree(head); + ret = WS_FATAL_ERROR; break; } +#ifndef _WIN32 + s_adv_mode = vectors[i].mode; + savedGetpwnam = wsshd_getpwnam_cb; + wsshd_getpwnam_cb = stub_getpwnam_adv; + InstallGroupStubs(0, &savedGrouplist, &savedSetgroups); + s_grouplist_always_fail = (vectors[i].mode == ADV_GROUPS_FAIL); +#endif + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); ssh = (ctx != NULL) ? wolfSSH_new(ctx) : NULL; - if (ssh == NULL || wolfSSH_SetUsername(ssh, usr) != WS_SUCCESS) { + if (ssh == NULL || + wolfSSH_SetUsername(ssh, UNKNOWN_USER) != WS_SUCCESS) { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + else if (DefaultUserAuthTypes(ssh, authCtx) != vectors[i].expected) { Log(" FAILED.\n"); ret = WS_FATAL_ERROR; } else { - expected = wolfSSHD_GetUserAuthTypes(conf); - if (expected == 0 || - DefaultUserAuthTypes(ssh, authCtx) != expected) { - Log(" FAILED.\n"); - ret = WS_FATAL_ERROR; - } - else { - Log(" PASSED.\n"); - } + Log(" PASSED.\n"); } +#ifndef _WIN32 + wsshd_getpwnam_cb = savedGetpwnam; + wsshd_getgrouplist_cb = savedGrouplist; + wsshd_setgroups_cb = savedSetgroups; +#endif wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); wolfSSHD_AuthFreeUser(authCtx); - wolfSSHD_ConfigFree(conf); + wolfSSHD_ConfigFree(head); } return ret; } +#undef UNKNOWN_USER + #ifndef _WIN32 /* report a single secure-open scenario; returns WS_SUCCESS when the observed * result matches expectation (wantOk != 0 means expect acceptance) */