Skip to content

Commit feaa9e4

Browse files
wolfsshd: add StrictModes permission/ownership checks for authorized_keys and host key
1 parent 2d0ef5a commit feaa9e4

8 files changed

Lines changed: 561 additions & 5 deletions

File tree

apps/wolfsshd/auth.c

Lines changed: 188 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969

7070
#ifndef _WIN32
7171
#include <sys/types.h>
72+
#include <sys/stat.h>
7273
#include <pwd.h>
7374
#include <grp.h>
7475
#include <errno.h>
@@ -526,8 +527,176 @@ static int ResolveAuthKeysPath(const char* homeDir, const char* pattern,
526527
return ret;
527528
}
528529

530+
/* OpenSSH "StrictModes" style permission/ownership check. Verifies that 'path'
531+
* is not group or world writable. When 'checkOwner' is set the file (and any
532+
* walked directory) must also be owned by 'uid' or by root. When 'checkChain'
533+
* is set, each parent directory up to and including 'homeDir' is checked the
534+
* same way (a writable ancestor directory would let an attacker substitute the
535+
* file). When 'noReadOthers' is set (used for secret files such as the host
536+
* private key) the file is also rejected if it is group or world readable.
537+
*
538+
* Public keys are not secret, so for authorized_keys we guard against write
539+
* access (key injection) and enforce ownership by the user. For the host
540+
* private key we guard against disclosure; ownership is not enforced there
541+
* because the server may run privileged (e.g. via sudo) against a key owned by
542+
* an unprivileged service account.
543+
*
544+
* Returns WS_SUCCESS when the path is considered safe. On platforms without
545+
* POSIX ownership/mode semantics this is a no-op returning WS_SUCCESS. */
546+
int wolfSSHD_CheckFilePermissions(const char* path, const char* homeDir,
547+
WUID_T uid, int checkOwner, int checkChain, int noReadOthers)
548+
{
549+
#ifndef _WIN32
550+
int ret = WS_SUCCESS;
551+
int fileInHome;
552+
struct stat s;
553+
char pathBuf[MAX_PATH_SZ];
554+
char* slash;
555+
word32 pathSz;
556+
word32 homeLen;
557+
word32 i;
558+
559+
if (path == NULL) {
560+
ret = WS_BAD_ARGUMENT;
561+
}
562+
563+
/* check the target file itself. stat() (not lstat()) is used so symlinked
564+
* key paths resolve to their target and are validated, rather than being
565+
* rejected outright. */
566+
if (ret == WS_SUCCESS) {
567+
if (stat(path, &s) != 0) {
568+
wolfSSH_Log(WS_LOG_ERROR,
569+
"[SSHD] Unable to stat %s for permission check", path);
570+
ret = WS_BAD_FILE_E;
571+
}
572+
else if (!S_ISREG(s.st_mode)) {
573+
wolfSSH_Log(WS_LOG_ERROR,
574+
"[SSHD] %s is not a regular file", path);
575+
ret = WS_BAD_FILE_E;
576+
}
577+
}
578+
if (ret == WS_SUCCESS && checkOwner) {
579+
if (s.st_uid != uid && s.st_uid != 0) {
580+
wolfSSH_Log(WS_LOG_ERROR,
581+
"[SSHD] Bad owner on %s, must be owned by the user or root",
582+
path);
583+
ret = WS_BAD_FILE_E;
584+
}
585+
}
586+
if (ret == WS_SUCCESS) {
587+
if ((s.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
588+
wolfSSH_Log(WS_LOG_ERROR,
589+
"[SSHD] %s is group or world writable", path);
590+
ret = WS_BAD_FILE_E;
591+
}
592+
}
593+
if (ret == WS_SUCCESS && noReadOthers) {
594+
if ((s.st_mode & (S_IRGRP | S_IROTH)) != 0) {
595+
wolfSSH_Log(WS_LOG_ERROR,
596+
"[SSHD] %s is group or world readable", path);
597+
ret = WS_BAD_FILE_E;
598+
}
599+
}
600+
601+
/* Walk parent directories checking that none is group or world writable,
602+
* since a writable ancestor would let another user rename it and substitute
603+
* the key file. When the file is inside the user's home directory the walk
604+
* stops at the home directory and also enforces ownership (the user/root
605+
* must own the file and each directory down from home). When the file is a
606+
* custom AuthorizedKeysFile outside the home directory, the walk continues
607+
* to the filesystem root but only checks writability, not ownership, since
608+
* those ancestors are system-managed and not owned by the user.
609+
*
610+
* homeLen is normalized to ignore any trailing slash on homeDir so the
611+
* subtree test does not depend on how pw_dir is formatted. */
612+
homeLen = (homeDir != NULL) ? (word32)WSTRLEN(homeDir) : 0;
613+
while (homeLen > 0 && homeDir[homeLen - 1] == '/') {
614+
homeLen--;
615+
}
616+
fileInHome = (homeDir != NULL &&
617+
WSTRNCMP(path, homeDir, homeLen) == 0 &&
618+
(path[homeLen] == '/' || path[homeLen] == '\0'));
619+
if (ret == WS_SUCCESS && checkChain && homeDir != NULL) {
620+
pathSz = (word32)WSTRLEN(path);
621+
if (pathSz >= MAX_PATH_SZ) {
622+
ret = WS_BAD_FILE_E;
623+
}
624+
else {
625+
WMEMCPY(pathBuf, path, pathSz);
626+
pathBuf[pathSz] = '\0';
627+
628+
while (ret == WS_SUCCESS) {
629+
/* trim the last path component to move up one directory */
630+
slash = NULL;
631+
for (i = 0; pathBuf[i] != '\0'; i++) {
632+
if (pathBuf[i] == '/') {
633+
slash = &pathBuf[i];
634+
}
635+
}
636+
if (slash == NULL) {
637+
break; /* no more parent directories */
638+
}
639+
if (slash == pathBuf) {
640+
pathBuf[1] = '\0'; /* parent is root "/" */
641+
}
642+
else {
643+
*slash = '\0';
644+
}
645+
646+
if (stat(pathBuf, &s) != 0) {
647+
wolfSSH_Log(WS_LOG_ERROR,
648+
"[SSHD] Unable to stat directory %s", pathBuf);
649+
ret = WS_BAD_FILE_E;
650+
break;
651+
}
652+
if (!S_ISDIR(s.st_mode)) {
653+
wolfSSH_Log(WS_LOG_ERROR,
654+
"[SSHD] %s is not a directory", pathBuf);
655+
ret = WS_BAD_FILE_E;
656+
break;
657+
}
658+
if (checkOwner && fileInHome &&
659+
s.st_uid != uid && s.st_uid != 0) {
660+
wolfSSH_Log(WS_LOG_ERROR,
661+
"[SSHD] Bad owner on directory %s", pathBuf);
662+
ret = WS_BAD_FILE_E;
663+
break;
664+
}
665+
if ((s.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
666+
wolfSSH_Log(WS_LOG_ERROR,
667+
"[SSHD] Directory %s is group or world writable",
668+
pathBuf);
669+
ret = WS_BAD_FILE_E;
670+
break;
671+
}
672+
673+
/* stop at the home directory (in-home files) or the
674+
* filesystem root (out-of-home files) */
675+
if ((fileInHome &&
676+
WSTRNCMP(pathBuf, homeDir, homeLen) == 0 &&
677+
pathBuf[homeLen] == '\0') ||
678+
WSTRCMP(pathBuf, "/") == 0) {
679+
break;
680+
}
681+
}
682+
}
683+
}
684+
685+
return ret;
686+
#else
687+
WOLFSSH_UNUSED(path);
688+
WOLFSSH_UNUSED(homeDir);
689+
WOLFSSH_UNUSED(uid);
690+
WOLFSSH_UNUSED(checkOwner);
691+
WOLFSSH_UNUSED(checkChain);
692+
WOLFSSH_UNUSED(noReadOthers);
693+
return WS_SUCCESS;
694+
#endif
695+
}
696+
529697
static int SearchForPubKey(const char* path, const char* authKeysFile,
530-
const WS_UserAuthData_PublicKey* pubKeyCtx)
698+
const WS_UserAuthData_PublicKey* pubKeyCtx,
699+
WUID_T uid, int strictModes)
531700
{
532701
int ret = WSSHD_AUTH_SUCCESS;
533702
char authKeysPath[MAX_PATH_SZ];
@@ -546,6 +715,20 @@ static int SearchForPubKey(const char* path, const char* authKeysFile,
546715
ret = rc;
547716
}
548717

718+
/* When StrictModes is enabled, refuse the authorized keys file if it or any
719+
* parent directory up to the home directory is owned by another user or is
720+
* group/world writable. */
721+
if (ret == WSSHD_AUTH_SUCCESS && strictModes) {
722+
if (wolfSSHD_CheckFilePermissions(authKeysPath, path, uid,
723+
1 /* checkOwner */, 1 /* checkChain */, 0 /* noReadOthers */)
724+
!= WS_SUCCESS) {
725+
wolfSSH_Log(WS_LOG_ERROR,
726+
"[SSHD] Authorized keys file %s failed StrictModes check",
727+
authKeysPath);
728+
ret = WSSHD_AUTH_FAILURE;
729+
}
730+
}
731+
549732
if (ret == WSSHD_AUTH_SUCCESS) {
550733
if (WFOPEN(NULL, &f, authKeysPath, "rb") != 0) {
551734
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Unable to open %s",
@@ -705,7 +888,8 @@ static int CheckPublicKeyUnix(const char* name,
705888
}
706889

707890
if (ret == WSSHD_AUTH_SUCCESS) {
708-
ret = SearchForPubKey(pwInfo->pw_dir, authorizedKeysFile, pubKeyCtx);
891+
ret = SearchForPubKey(pwInfo->pw_dir, authorizedKeysFile, pubKeyCtx,
892+
pwInfo->pw_uid, wolfSSHD_ConfigGetStrictModes(authCtx->conf));
709893
}
710894
}
711895

@@ -1049,7 +1233,8 @@ static int CheckPublicKeyWIN(const char* usr,
10491233
if (ret == WSSHD_AUTH_SUCCESS) {
10501234
r[rSz-1] = L'\0';
10511235

1052-
ret = SearchForPubKey(r, authorizedKeysFile, pubKeyCtx);
1236+
ret = SearchForPubKey(r, authorizedKeysFile, pubKeyCtx, 0,
1237+
wolfSSHD_ConfigGetStrictModes(authCtx->conf));
10531238
if (ret != WSSHD_AUTH_SUCCESS) {
10541239
wolfSSH_Log(WS_LOG_ERROR,
10551240
"[SSHD] Failed to find public key for user %s", usr);

apps/wolfsshd/auth.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ HANDLE wolfSSHD_GetAuthToken(const WOLFSSHD_AUTH* auth);
8080
int wolfSSHD_GetHomeDirectory(WOLFSSHD_AUTH* auth, WOLFSSH* ssh, WCHAR* out, int outSz);
8181
#endif
8282

83+
/* StrictModes permission/ownership check, shared by the authorized_keys path
84+
* (auth.c) and the host private key load (wolfsshd.c). See the definition in
85+
* auth.c for the meaning of each flag. */
86+
int wolfSSHD_CheckFilePermissions(const char* path, const char* homeDir,
87+
WUID_T uid, int checkOwner, int checkChain, int noReadOthers);
88+
8389
#ifdef WOLFSSHD_UNIT_TEST
8490
#ifndef _WIN32
8591
extern int (*wsshd_setregid_cb)(WGID_T, WGID_T);

apps/wolfsshd/configuration.c

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ struct WOLFSSHD_CONFIG {
8989
byte permitRootLogin:1;
9090
byte permitEmptyPasswords:1;
9191
byte authKeysFileSet:1; /* if not set then no explicit authorized keys */
92+
byte strictModes:1; /* enforce file permission/ownership checks */
9293
};
9394

9495
int CountWhitespace(const char* in, int inSz, byte inv);
@@ -218,6 +219,7 @@ WOLFSSHD_CONFIG* wolfSSHD_ConfigNew(void* heap)
218219
ret->port = 22;
219220
ret->passwordAuth = 1;
220221
ret->loginTimer = 120;
222+
ret->strictModes = 1; /* on by default, matching OpenSSH */
221223
}
222224
return ret;
223225

@@ -315,6 +317,7 @@ static WOLFSSHD_CONFIG* wolfSSHD_ConfigCopy(WOLFSSHD_CONFIG* conf)
315317
newConf->permitRootLogin = conf->permitRootLogin;
316318
newConf->permitEmptyPasswords = conf->permitEmptyPasswords;
317319
newConf->authKeysFileSet = conf->authKeysFileSet;
320+
newConf->strictModes = conf->strictModes;
318321
}
319322
else {
320323
wolfSSHD_ConfigFree(newConf);
@@ -388,9 +391,10 @@ enum {
388391
OPT_TRUSTED_USER_CA_KEYS = 21,
389392
OPT_PIDFILE = 22,
390393
OPT_BANNER = 23,
394+
OPT_STRICT_MODES = 24,
391395
};
392396
enum {
393-
NUM_OPTIONS = 24
397+
NUM_OPTIONS = 25
394398
};
395399

396400
static const CONFIG_OPTION options[NUM_OPTIONS] = {
@@ -418,6 +422,7 @@ static const CONFIG_OPTION options[NUM_OPTIONS] = {
418422
{OPT_TRUSTED_USER_CA_KEYS, "TrustedUserCAKeys"},
419423
{OPT_PIDFILE, "PidFile"},
420424
{OPT_BANNER, "Banner"},
425+
{OPT_STRICT_MODES, "StrictModes"},
421426
};
422427

423428
/* returns WS_SUCCESS on success */
@@ -553,6 +558,31 @@ static int HandlePwAuth(WOLFSSHD_CONFIG* conf, const char* value)
553558
return ret;
554559
}
555560

561+
/* returns WS_SUCCESS on success */
562+
static int HandleStrictModes(WOLFSSHD_CONFIG* conf, const char* value)
563+
{
564+
int ret = WS_SUCCESS;
565+
566+
if (conf == NULL || value == NULL) {
567+
ret = WS_BAD_ARGUMENT;
568+
}
569+
570+
if (ret == WS_SUCCESS) {
571+
if (WSTRCMP(value, "no") == 0) {
572+
wolfSSH_Log(WS_LOG_INFO, "[SSHD] StrictModes disabled");
573+
conf->strictModes = 0;
574+
}
575+
else if (WSTRCMP(value, "yes") == 0) {
576+
conf->strictModes = 1;
577+
}
578+
else {
579+
ret = WS_BAD_ARGUMENT;
580+
}
581+
}
582+
583+
return ret;
584+
}
585+
556586
#define WOLFSSH_PROTOCOL_VERSION 2
557587

558588
/* returns WS_SUCCESS on success */
@@ -1065,6 +1095,9 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt,
10651095
case OPT_BANNER:
10661096
ret = SetFileString(&(*conf)->banner, value, (*conf)->heap);
10671097
break;
1098+
case OPT_STRICT_MODES:
1099+
ret = HandleStrictModes(*conf, value);
1100+
break;
10681101
default:
10691102
break;
10701103
}
@@ -1278,6 +1311,19 @@ int wolfSSHD_ConfigGetAuthKeysFileSet(const WOLFSSHD_CONFIG* conf)
12781311
return ret;
12791312
}
12801313

1314+
/* returns 1 if StrictModes is enabled and 0 if not. Defaults to enabled (fail
1315+
* safe) when conf is NULL. */
1316+
int wolfSSHD_ConfigGetStrictModes(const WOLFSSHD_CONFIG* conf)
1317+
{
1318+
int ret = 1;
1319+
1320+
if (conf != NULL) {
1321+
ret = conf->strictModes;
1322+
}
1323+
1324+
return ret;
1325+
}
1326+
12811327
int wolfSSHD_ConfigSetAuthKeysFile(WOLFSSHD_CONFIG* conf, const char* file)
12821328
{
12831329
int ret = WS_SUCCESS;

apps/wolfsshd/configuration.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ word16 wolfSSHD_ConfigGetPort(const WOLFSSHD_CONFIG* conf);
4747
char* wolfSSHD_ConfigGetAuthKeysFile(const WOLFSSHD_CONFIG* conf);
4848
int wolfSSHD_ConfigGetAuthKeysFileSet(const WOLFSSHD_CONFIG* conf);
4949
int wolfSSHD_ConfigSetAuthKeysFile(WOLFSSHD_CONFIG* conf, const char* file);
50+
int wolfSSHD_ConfigGetStrictModes(const WOLFSSHD_CONFIG* conf);
5051
byte wolfSSHD_ConfigGetPermitEmptyPw(const WOLFSSHD_CONFIG* conf);
5152
byte wolfSSHD_ConfigGetPermitRoot(const WOLFSSHD_CONFIG* conf);
5253
byte wolfSSHD_ConfigGetPrivilegeSeparation(const WOLFSSHD_CONFIG* conf);

0 commit comments

Comments
 (0)