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>
76+ #include <fcntl.h>
77+ #include <unistd.h>
78+ #include <limits.h>
79+ #include <stdlib.h>
80+ #ifndef O_NOFOLLOW
81+ /* Older platforms lack O_NOFOLLOW; the lstat() pre-check and the post-open
82+ * st_dev/st_ino comparison still reject a symlinked leaf there. */
83+ #define O_NOFOLLOW 0
84+ #endif
85+ #ifndef PATH_MAX
86+ #define PATH_MAX 4096
87+ #endif
7588#endif
7689
7790#if !defined(_WIN32 ) && !(defined(__OSX__ ) || defined(__APPLE__ ))
@@ -526,8 +539,225 @@ static int ResolveAuthKeysPath(const char* homeDir, const char* pattern,
526539 return ret ;
527540}
528541
542+ /* Securely open a trusted file, failing closed on a symlink, bad ownership, or
543+ * unsafe permissions, and hand back an open stream ready for reading. This is
544+ * the single gate for every security-critical file wolfsshd loads: a user's
545+ * authorized_keys, the host private key, the host certificate, and the user
546+ * certificate-authority keys.
547+ *
548+ * path - file to open.
549+ * ownerUid - the file itself must be owned by this user id or by root
550+ * (0). authorized_keys uses the owning user's id; the
551+ * daemon's trust anchors use the effective user id. Parent
552+ * directories are checked for writability but not ownership,
553+ * so a file may legitimately live under a directory owned by
554+ * a third party (e.g. a key under a build checkout or a
555+ * service account's tree).
556+ * rejectReadable - when set, also refuse a file that is group or world
557+ * readable. Used for secrets such as the host private key.
558+ * heap - heap hint for the temporary path buffer.
559+ * out - set to the open stream on success, WBADFILE otherwise.
560+ *
561+ * Returns WS_SUCCESS and sets *out on success; a specific reason is logged on
562+ * failure. On platforms without POSIX ownership semantics (_WIN32) the checks
563+ * are skipped and the file is opened directly, relying on filesystem ACLs. */
564+ int wolfSSHD_OpenSecureFile (const char * path , WUID_T ownerUid ,
565+ int rejectReadable , void * heap , WFILE * * out )
566+ {
567+ #ifndef _WIN32
568+ int ret = WS_SUCCESS ;
569+ int fd = -1 ;
570+ int flags ;
571+ struct stat lst ;
572+ struct stat st ;
573+ WFILE * f ;
574+ char * resolved = NULL ;
575+ char * slash ;
576+ word32 i ;
577+
578+ if (path == NULL || out == NULL ) {
579+ return WS_BAD_ARGUMENT ;
580+ }
581+ * out = WBADFILE ;
582+
583+ /* The leaf must be a real, regular file. lstat() (not stat()) is used so a
584+ * symlinked leaf is rejected outright rather than silently followed to an
585+ * attacker-chosen target. */
586+ if (lstat (path , & lst ) != 0 || !S_ISREG (lst .st_mode )) {
587+ wolfSSH_Log (WS_LOG_ERROR ,
588+ "[SSHD] Refusing to load %s: missing, not a regular file, or a "
589+ "symlink" , path );
590+ ret = WS_BAD_FILE_E ;
591+ }
592+
593+ /* Canonicalize the path with realpath(), resolving any intermediate
594+ * symlinks, then open and validate that canonical path so the file opened
595+ * and the parent chain validated below are one and the same. */
596+ if (ret == WS_SUCCESS ) {
597+ resolved = (char * )WMALLOC (PATH_MAX , heap , DYNTYPE_BUFFER );
598+ if (resolved == NULL ) {
599+ ret = WS_MEMORY_E ;
600+ }
601+ }
602+ if (ret == WS_SUCCESS ) {
603+ if (realpath (path , resolved ) == NULL ) {
604+ wolfSSH_Log (WS_LOG_ERROR , "[SSHD] Unable to resolve path %s" , path );
605+ ret = WS_BAD_FILE_E ;
606+ }
607+ }
608+
609+ /* Open the canonicalized path (not the original) so the directory chain
610+ * validated below is exactly the chain open() traverses. realpath() already
611+ * resolved every intermediate symlink; O_NOFOLLOW guards the
612+ * already-verified non-symlink leaf, and O_NONBLOCK keeps the open from
613+ * stalling on a FIFO swapped in after the lstat() and is cleared before the
614+ * buffered reads. The original path is used only in log messages. */
615+ if (ret == WS_SUCCESS ) {
616+ fd = open (resolved , O_RDONLY | O_NOFOLLOW | O_NONBLOCK );
617+ if (fd < 0 ) {
618+ wolfSSH_Log (WS_LOG_ERROR , "[SSHD] Unable to open %s" , path );
619+ ret = WS_BAD_FILE_E ;
620+ }
621+ }
622+ if (ret == WS_SUCCESS ) {
623+ if (fstat (fd , & st ) != 0 ) {
624+ wolfSSH_Log (WS_LOG_ERROR , "[SSHD] Unable to stat %s" , path );
625+ ret = WS_BAD_FILE_E ;
626+ }
627+ }
628+ /* The ownership and mode checks run on the opened descriptor so there is no
629+ * window to swap the file after the check. Comparing st_dev/st_ino against
630+ * the earlier lstat() closes the narrow swap window on platforms where
631+ * O_NOFOLLOW is unavailable and compiles to 0. */
632+ if (ret == WS_SUCCESS ) {
633+ if (!S_ISREG (st .st_mode )) {
634+ wolfSSH_Log (WS_LOG_ERROR ,
635+ "[SSHD] Refusing to load %s: not a regular file" , path );
636+ ret = WS_BAD_FILE_E ;
637+ }
638+ else if (st .st_uid != ownerUid && st .st_uid != 0 ) {
639+ wolfSSH_Log (WS_LOG_ERROR ,
640+ "[SSHD] Refusing to load %s: not owned by the user or root" ,
641+ path );
642+ ret = WS_BAD_FILE_E ;
643+ }
644+ else if ((st .st_mode & (S_IWGRP | S_IWOTH )) != 0 ) {
645+ wolfSSH_Log (WS_LOG_ERROR ,
646+ "[SSHD] Refusing to load %s: group or world writable" , path );
647+ ret = WS_BAD_FILE_E ;
648+ }
649+ else if (rejectReadable && (st .st_mode & (S_IRGRP | S_IROTH )) != 0 ) {
650+ wolfSSH_Log (WS_LOG_ERROR ,
651+ "[SSHD] Refusing to load %s: group or world readable" , path );
652+ ret = WS_BAD_FILE_E ;
653+ }
654+ else if (st .st_dev != lst .st_dev || st .st_ino != lst .st_ino ) {
655+ wolfSSH_Log (WS_LOG_ERROR ,
656+ "[SSHD] Refusing to load %s: file changed during open" , path );
657+ ret = WS_BAD_FILE_E ;
658+ }
659+ }
660+
661+ /* Validate every parent directory of the canonicalized path up to the
662+ * filesystem root: none may be group or world writable (unless sticky),
663+ * which is what would let another user rename the file and swap it. Ancestor
664+ * ownership is not enforced; the leaf owner check above is what stops a file
665+ * owned by a third party from being loaded. Since realpath() resolved all
666+ * intermediate symlinks, this is the same chain open() traversed. The walk
667+ * trims components from 'resolved' in place, which is fine now that the file
668+ * is already open. */
669+ while (ret == WS_SUCCESS ) {
670+ /* trim the last component to move up one directory */
671+ slash = NULL ;
672+ for (i = 0 ; resolved [i ] != '\0' ; i ++ ) {
673+ if (resolved [i ] == '/' ) {
674+ slash = & resolved [i ];
675+ }
676+ }
677+ if (slash == NULL ) {
678+ break ; /* no further parent (realpath always returns an absolute
679+ * path, so this is not expected) */
680+ }
681+ if (slash == resolved ) {
682+ resolved [1 ] = '\0' ; /* parent is the root directory "/" */
683+ }
684+ else {
685+ * slash = '\0' ;
686+ }
687+
688+ if (stat (resolved , & st ) != 0 ) {
689+ wolfSSH_Log (WS_LOG_ERROR ,
690+ "[SSHD] Unable to stat directory %s" , resolved );
691+ ret = WS_BAD_FILE_E ;
692+ }
693+ else if (!S_ISDIR (st .st_mode )) {
694+ wolfSSH_Log (WS_LOG_ERROR ,
695+ "[SSHD] %s is not a directory" , resolved );
696+ ret = WS_BAD_FILE_E ;
697+ }
698+ else if ((st .st_mode & (S_IWGRP | S_IWOTH )) != 0 &&
699+ (st .st_mode & S_ISVTX ) == 0 ) {
700+ /* A world/group writable directory is unsafe unless it is sticky:
701+ * the sticky bit stops a non-owner from renaming or deleting files
702+ * they do not own, which is exactly the substitution this guards
703+ * against (e.g. /tmp is mode 1777). */
704+ wolfSSH_Log (WS_LOG_ERROR ,
705+ "[SSHD] Directory %s is group or world writable" , resolved );
706+ ret = WS_BAD_FILE_E ;
707+ }
708+
709+ if (ret != WS_SUCCESS || WSTRCMP (resolved , "/" ) == 0 ) {
710+ break ; /* reached the filesystem root */
711+ }
712+ }
713+
714+ /* The target is a regular file, so restore blocking semantics for the
715+ * buffered reads the caller will perform. */
716+ if (ret == WS_SUCCESS ) {
717+ flags = fcntl (fd , F_GETFL );
718+ if (flags != -1 ) {
719+ (void )fcntl (fd , F_SETFL , flags & ~O_NONBLOCK );
720+ }
721+ f = fdopen (fd , "rb" );
722+ if (f == NULL ) {
723+ wolfSSH_Log (WS_LOG_ERROR ,
724+ "[SSHD] Unable to open stream for %s" , path );
725+ ret = WS_BAD_FILE_E ;
726+ }
727+ else {
728+ fd = -1 ; /* ownership of the descriptor moved to the stream */
729+ * out = f ;
730+ }
731+ }
732+
733+ if (fd >= 0 ) {
734+ close (fd );
735+ }
736+ if (resolved != NULL ) {
737+ WFREE (resolved , heap , DYNTYPE_BUFFER );
738+ }
739+
740+ return ret ;
741+ #else
742+ WOLFSSH_UNUSED (ownerUid );
743+ WOLFSSH_UNUSED (rejectReadable );
744+ WOLFSSH_UNUSED (heap );
745+
746+ if (path == NULL || out == NULL ) {
747+ return WS_BAD_ARGUMENT ;
748+ }
749+ * out = WBADFILE ;
750+ if (WFOPEN (NULL , out , path , "rb" ) != 0 ) {
751+ wolfSSH_Log (WS_LOG_ERROR , "[SSHD] Unable to open %s" , path );
752+ return WS_BAD_FILE_E ;
753+ }
754+ return WS_SUCCESS ;
755+ #endif
756+ }
757+
529758static int SearchForPubKey (const char * path , const char * authKeysFile ,
530- const WS_UserAuthData_PublicKey * pubKeyCtx )
759+ const WS_UserAuthData_PublicKey * pubKeyCtx ,
760+ WUID_T uid , int strictModes )
531761{
532762 int ret = WSSHD_AUTH_SUCCESS ;
533763 char authKeysPath [MAX_PATH_SZ ];
@@ -546,8 +776,21 @@ static int SearchForPubKey(const char* path, const char* authKeysFile,
546776 ret = rc ;
547777 }
548778
779+ /* When StrictModes is enabled, open through the secure gate: the file must
780+ * be a regular file (no symlink), owned by the user or root, with no
781+ * group/world writable component in its path. When disabled, fall back to a
782+ * plain open. */
549783 if (ret == WSSHD_AUTH_SUCCESS ) {
550- if (WFOPEN (NULL , & f , authKeysPath , "rb" ) != 0 ) {
784+ if (strictModes ) {
785+ if (wolfSSHD_OpenSecureFile (authKeysPath , uid ,
786+ 0 /* rejectReadable */ , NULL , & f ) != WS_SUCCESS ) {
787+ wolfSSH_Log (WS_LOG_ERROR ,
788+ "[SSHD] Authorized keys file %s failed StrictModes check" ,
789+ authKeysPath );
790+ ret = WSSHD_AUTH_FAILURE ;
791+ }
792+ }
793+ else if (WFOPEN (NULL , & f , authKeysPath , "rb" ) != 0 ) {
551794 wolfSSH_Log (WS_LOG_ERROR , "[SSHD] Unable to open %s" ,
552795 authKeysPath );
553796 ret = WS_BAD_FILE_E ;
@@ -593,6 +836,10 @@ static int SearchForPubKey(const char* path, const char* authKeysFile,
593836 WFCLOSE (NULL , f );
594837 }
595838
839+ if (lineBuf != NULL ) {
840+ WFREE (lineBuf , NULL , DYNTYPE_BUFFER );
841+ }
842+
596843 if (ret == WSSHD_AUTH_SUCCESS && !foundKey ) {
597844 ret = WSSHD_AUTH_FAILURE ;
598845 }
@@ -705,7 +952,8 @@ static int CheckPublicKeyUnix(const char* name,
705952 }
706953
707954 if (ret == WSSHD_AUTH_SUCCESS ) {
708- ret = SearchForPubKey (pwInfo -> pw_dir , authorizedKeysFile , pubKeyCtx );
955+ ret = SearchForPubKey (pwInfo -> pw_dir , authorizedKeysFile , pubKeyCtx ,
956+ pwInfo -> pw_uid , wolfSSHD_ConfigGetStrictModes (authCtx -> conf ));
709957 }
710958 }
711959
@@ -1049,7 +1297,8 @@ static int CheckPublicKeyWIN(const char* usr,
10491297 if (ret == WSSHD_AUTH_SUCCESS ) {
10501298 r [rSz - 1 ] = L'\0' ;
10511299
1052- ret = SearchForPubKey (r , authorizedKeysFile , pubKeyCtx );
1300+ ret = SearchForPubKey (r , authorizedKeysFile , pubKeyCtx , 0 ,
1301+ wolfSSHD_ConfigGetStrictModes (authCtx -> conf ));
10531302 if (ret != WSSHD_AUTH_SUCCESS ) {
10541303 wolfSSH_Log (WS_LOG_ERROR ,
10551304 "[SSHD] Failed to find public key for user %s" , usr );
0 commit comments