@@ -2455,7 +2455,10 @@ static int GetFileStats(void *fs, ScpSendCtx* ctx, const char* fileName,
24552455 (ctx -> s .dwFileAttributes & FILE_ATTRIBUTE_READONLY ? 0 : 0200 );
24562456 * fileMode |= (ctx -> s .dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) ? 040000 : 0 ;
24572457#else
2458- if (WSTAT (fs , fileName , & ctx -> s ) < 0 ) {
2458+ /* WLSTAT (lstat on POSIX) does not follow a symlink, so the metadata and
2459+ * the dir/file classification below reflect the link itself, not its
2460+ * target; a symlink is then neither dir nor file and is not traversed. */
2461+ if (WLSTAT (fs , fileName , & ctx -> s ) < 0 ) {
24592462 ret = WS_BAD_FILE_E ;
24602463 #ifdef WOLFSSL_NUCLEUS
24612464 if (WSTRLEN (fileName ) < 4 && WSTRLEN (fileName ) > 2 &&
@@ -2543,11 +2546,16 @@ static ScpDir* ScpNewDir(void *fs, const char* path, void* heap)
25432546 }
25442547 }
25452548#else
2549+ #ifdef WOLFSSH_HAVE_SYMLINK
2550+ /* refuse a symlinked directory leaf atomically (closes the descend race) */
2551+ if (wOpendirNoFollow (fs , & entry -> dir , path ) != 0 ) {
2552+ #else
25462553 if (WOPENDIR (fs , heap , & entry -> dir , path ) != 0
25472554 #if !defined (WOLFSSL_NUCLEUS ) && !defined (WOLFSSH_ZEPHYR )
25482555 || entry -> dir == NULL
25492556 #endif
25502557 ) {
2558+ #endif
25512559 WFREE (entry , heap , DYNTYPE_SCPDIR );
25522560 WLOG (WS_LOG_ERROR , scpError , "opendir failed on directory" ,
25532561 WS_INVALID_PATH_E );
@@ -2626,6 +2634,17 @@ int ScpPopDir(void *fs, ScpSendCtx* ctx, void* heap)
26262634 return WS_SUCCESS ;
26272635}
26282636
2637+ /* Drain dir-stack entries (and open dir handles) left on a send context after
2638+ * a recursive transfer aborts mid-tree before popping. Safe on an empty
2639+ * stack. */
2640+ void ScpSendCtxFreeDirs (void * fs , ScpSendCtx * ctx , void * heap )
2641+ {
2642+ if (ctx != NULL ) {
2643+ while (ctx -> currentDir != NULL )
2644+ (void )ScpPopDir (fs , ctx , heap );
2645+ }
2646+ }
2647+
26292648/* Get next entry in directory, either file or directory, skips self (.)
26302649 * and parent (..) directories, stores in ctx->entry.
26312650 * Return WS_SUCCESS on success or negative upon error */
@@ -2820,6 +2839,16 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
28202839 WSTRNCPY (fileName , sendCtx -> entry -> d_name ,
28212840 DEFAULT_SCP_FILE_NAME_SZ );
28222841 #endif
2842+ #ifdef WOLFSSH_HAVE_SYMLINK
2843+ /* filePath is fully built; reject a planted symlink before
2844+ * GetFileStats or any descend/open follows it. */
2845+ if (ret == WS_SUCCESS && wIsSymlink (filePath )) {
2846+ WLOG (WS_LOG_ERROR ,
2847+ "scp: symlink entry rejected, aborting transfer" );
2848+ ret = WS_SCP_ABORT ;
2849+ }
2850+ #endif /* WOLFSSH_HAVE_SYMLINK */
2851+
28232852 if (ret == WS_SUCCESS ) {
28242853 ret = GetFileStats (ssh -> fs , sendCtx , filePath , mTime , aTime , fileMode );
28252854 }
@@ -2839,7 +2868,11 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
28392868 }
28402869
28412870 } else if (ScpFileIsFile (sendCtx )) {
2871+ #ifdef WOLFSSH_HAVE_SYMLINK
2872+ if (wFopenNoFollow (ssh -> fs , & (sendCtx -> fp ), filePath ) != 0 ) {
2873+ #else
28422874 if (WFOPEN (ssh -> fs , & (sendCtx -> fp ), filePath , "rb" ) != 0 ) {
2875+ #endif
28432876 WLOG (WS_LOG_ERROR , "scp: Error with opening file, abort" );
28442877 wolfSSH_SetScpErrorMsg (ssh , "unable to open file "
28452878 "for reading" );
@@ -2862,7 +2895,10 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
28622895 }
28632896
28642897 } else {
2865- if (ret != WS_NEXT_ERROR ) {
2898+ /* WS_SCP_ABORT entries (e.g. a rejected symlink) were already logged at
2899+ * their source, so only the generic, unexpected-error case is noted
2900+ * here to avoid a misleading second log line. */
2901+ if (ret != WS_NEXT_ERROR && ret != WS_SCP_ABORT ) {
28662902 WLOG (WS_LOG_ERROR , "scp: ret does not equal WS_NEXT_ERROR, abort" );
28672903 ret = WS_SCP_ABORT ;
28682904 }
@@ -2948,6 +2984,22 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
29482984 * is complete.
29492985 * WS_SCP_ABORT - abort file transfer request
29502986 * WS_BAD_FILE_E - local file open error hit
2987+ *
2988+ * Symlink handling: file-content opens go through wFopenNoFollow and directory
2989+ * opens (both the recursive root and every descend) go through wOpendirNoFollow.
2990+ * Both are atomic against a swapped link on POSIX (O_NOFOLLOW, plus O_DIRECTORY
2991+ * for the directory open) and fall back to a wIsSymlink check-then-open on
2992+ * Windows or where O_NOFOLLOW is absent. The root also gets an explicit
2993+ * wIsSymlink pre-check because a trailing separator (open("link/", O_NOFOLLOW))
2994+ * can still follow the link; symlinks below the root are rejected as
2995+ * ScpProcessEntry traverses them. No "stays under a trusted base" containment
2996+ * is attempted: SCP has no library-level base path (wolfsshd relies on OS
2997+ * chroot) and wolfSSH_RealPath does not resolve links. GetFileStats uses WLSTAT
2998+ * so it does not follow a link for metadata or classification. On the
2999+ * Windows/fallback path the open is check-then-open, so a concurrent in-jail
3000+ * writer racing it remains a best-effort gap. For hostile multi-tenant use,
3001+ * confine the session with an OS mechanism (chroot, dropped privileges) and
3002+ * treat these checks as defense in depth.
29513003 */
29523004int wsScpSendCallback (WOLFSSH * ssh , int state , const char * peerRequest ,
29533005 char * fileName , word32 fileNameSz , word64 * mTime , word64 * aTime ,
@@ -2981,9 +3033,14 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
29813033 break ;
29823034
29833035 case WOLFSSH_SCP_SINGLE_FILE_REQUEST :
2984- if ((sendCtx == NULL ) || WFOPEN (ssh -> fs , & (sendCtx -> fp ), peerRequest ,
2985- "rb" ) != 0 ) {
2986-
3036+ /* open without following a symlink so its target is not streamed
3037+ * to the peer; see this function's symlink-handling note. */
3038+ if ((sendCtx == NULL ) ||
3039+ #ifdef WOLFSSH_HAVE_SYMLINK
3040+ wFopenNoFollow (ssh -> fs , & (sendCtx -> fp ), peerRequest ) != 0 ) {
3041+ #else
3042+ WFOPEN (ssh -> fs , & (sendCtx -> fp ), peerRequest , "rb" ) != 0 ) {
3043+ #endif
29873044 WLOG (WS_LOG_ERROR , "scp: unable to open file, abort" );
29883045 wolfSSH_SetScpErrorMsg (ssh , "unable to open file for reading" );
29893046 ret = WS_BAD_FILE_E ;
@@ -3037,9 +3094,40 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
30373094 case WOLFSSH_SCP_RECURSIVE_REQUEST :
30383095
30393096 if (ScpDirStackIsEmpty (sendCtx )) {
3097+ #ifdef WOLFSSH_HAVE_SYMLINK
3098+ word32 rootLen ;
3099+ #endif
30403100
3041- /* first request, keep track of request directory */
3042- ret = ScpPushDir (ssh -> fs , sendCtx , peerRequest , ssh -> ctx -> heap );
3101+ /* first request, keep track of request directory. Reject a
3102+ * symlink root here (a trailing separator can still follow);
3103+ * see the symlink-handling note in this function's header. */
3104+ ret = WS_SUCCESS ;
3105+ #ifdef WOLFSSH_HAVE_SYMLINK
3106+ /* lstat() follows the link when the path ends in a separator,
3107+ * so check the root with any trailing separators removed */
3108+ rootLen = (word32 )WSTRLEN (peerRequest );
3109+ while (rootLen > 1 && (peerRequest [rootLen - 1 ] == '/' ||
3110+ peerRequest [rootLen - 1 ] == '\\' ))
3111+ rootLen -- ;
3112+ if (rootLen >= DEFAULT_SCP_FILE_NAME_SZ ) {
3113+ ret = WS_SCP_ABORT ;
3114+ }
3115+ else {
3116+ WMEMCPY (filePath , peerRequest , rootLen );
3117+ filePath [rootLen ] = '\0' ;
3118+ if (wIsSymlink (filePath )) {
3119+ WLOG (WS_LOG_ERROR ,
3120+ "scp: refusing recursive root symlink, abort" );
3121+ wolfSSH_SetScpErrorMsg (ssh ,
3122+ "unable to open file for reading" );
3123+ ret = WS_SCP_ABORT ;
3124+ }
3125+ }
3126+ #endif /* WOLFSSH_HAVE_SYMLINK */
3127+
3128+ if (ret == WS_SUCCESS )
3129+ ret = ScpPushDir (ssh -> fs , sendCtx , peerRequest ,
3130+ ssh -> ctx -> heap );
30433131
30443132 if (ret == WS_SUCCESS ) {
30453133 /* get file name from request */
@@ -3053,7 +3141,9 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
30533141
30543142 if (ret == WS_SUCCESS ) {
30553143 ret = WS_SCP_ENTER_DIR ;
3056- } else {
3144+ } else if (ret != WS_SCP_ABORT ) {
3145+ /* a rejected symlink root already logged its own reason;
3146+ * only note the generic stat failure here */
30573147 WLOG (WS_LOG_ERROR , "scp: error getting file stats, abort" );
30583148 ret = WS_SCP_ABORT ;
30593149 }
0 commit comments