@@ -646,6 +646,16 @@ int DoScpSource(WOLFSSH* ssh)
646646 continue ;
647647
648648 } else if (ssh -> scpConfirm == WS_SCP_ABORT ) {
649+ #if !defined(NO_FILESYSTEM ) && \
650+ !defined(WOLFSSH_SCP_USER_CALLBACKS )
651+ /* drain any partial recursive dir stack so a later exec on
652+ * this connection starts from a fresh root, not a stale
653+ * handle left by the aborted walk */
654+ ScpSendCtx * sendCtx =
655+ (ScpSendCtx * )wolfSSH_GetScpSendCtx (ssh );
656+ if (sendCtx != NULL )
657+ ScpSendCtxFreeDirs (ssh -> fs , sendCtx , ssh -> ctx -> heap );
658+ #endif
649659 ssh -> scpState = SCP_SEND_CONFIRMATION ;
650660 ssh -> scpNextState = SCP_DONE ;
651661 continue ;
@@ -2454,8 +2464,15 @@ static int GetFileStats(void *fs, ScpSendCtx* ctx, const char* fileName,
24542464 * fileMode = 0555 |
24552465 (ctx -> s .dwFileAttributes & FILE_ATTRIBUTE_READONLY ? 0 : 0200 );
24562466 * fileMode |= (ctx -> s .dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) ? 040000 : 0 ;
2467+ #else
2468+ /* WLSTAT (lstat on POSIX) leaves a symlink unfollowed, so it classifies as
2469+ * neither dir nor file and is skipped; WOLFSSH_NO_SYMLINK_CHECK falls back
2470+ * to WSTAT so links are followed by design. */
2471+ #ifdef WOLFSSH_HAVE_SYMLINK
2472+ if (WLSTAT (fs , fileName , & ctx -> s ) < 0 ) {
24572473#else
24582474 if (WSTAT (fs , fileName , & ctx -> s ) < 0 ) {
2475+ #endif
24592476 ret = WS_BAD_FILE_E ;
24602477 #ifdef WOLFSSL_NUCLEUS
24612478 if (WSTRLEN (fileName ) < 4 && WSTRLEN (fileName ) > 2 &&
@@ -2543,11 +2560,16 @@ static ScpDir* ScpNewDir(void *fs, const char* path, void* heap)
25432560 }
25442561 }
25452562#else
2563+ #ifdef WOLFSSH_HAVE_SYMLINK
2564+ /* refuse a symlinked directory leaf atomically (closes the descend race) */
2565+ if (wOpendirNoFollow (fs , & entry -> dir , path ) != 0 ) {
2566+ #else
25462567 if (WOPENDIR (fs , heap , & entry -> dir , path ) != 0
25472568 #if !defined (WOLFSSL_NUCLEUS ) && !defined (WOLFSSH_ZEPHYR )
25482569 || entry -> dir == NULL
25492570 #endif
25502571 ) {
2572+ #endif
25512573 WFREE (entry , heap , DYNTYPE_SCPDIR );
25522574 WLOG (WS_LOG_ERROR , scpError , "opendir failed on directory" ,
25532575 WS_INVALID_PATH_E );
@@ -2626,6 +2648,17 @@ int ScpPopDir(void *fs, ScpSendCtx* ctx, void* heap)
26262648 return WS_SUCCESS ;
26272649}
26282650
2651+ /* Drain dir-stack entries (and open dir handles) left on a send context after
2652+ * a recursive transfer aborts mid-tree before popping. Safe on an empty
2653+ * stack. */
2654+ void ScpSendCtxFreeDirs (void * fs , ScpSendCtx * ctx , void * heap )
2655+ {
2656+ if (ctx != NULL ) {
2657+ while (ctx -> currentDir != NULL )
2658+ (void )ScpPopDir (fs , ctx , heap );
2659+ }
2660+ }
2661+
26292662/* Get next entry in directory, either file or directory, skips self (.)
26302663 * and parent (..) directories, stores in ctx->entry.
26312664 * Return WS_SUCCESS on success or negative upon error */
@@ -2820,6 +2853,16 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
28202853 WSTRNCPY (fileName , sendCtx -> entry -> d_name ,
28212854 DEFAULT_SCP_FILE_NAME_SZ );
28222855 #endif
2856+ #ifdef WOLFSSH_HAVE_SYMLINK
2857+ /* filePath is fully built; reject a planted symlink before
2858+ * GetFileStats or any descend/open follows it. */
2859+ if (ret == WS_SUCCESS && wIsSymlink (filePath )) {
2860+ WLOG (WS_LOG_ERROR ,
2861+ "scp: symlink entry rejected, aborting transfer" );
2862+ ret = WS_SCP_ABORT ;
2863+ }
2864+ #endif /* WOLFSSH_HAVE_SYMLINK */
2865+
28232866 if (ret == WS_SUCCESS ) {
28242867 ret = GetFileStats (ssh -> fs , sendCtx , filePath , mTime , aTime , fileMode );
28252868 }
@@ -2839,7 +2882,11 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
28392882 }
28402883
28412884 } else if (ScpFileIsFile (sendCtx )) {
2885+ #ifdef WOLFSSH_HAVE_SYMLINK
2886+ if (wFopenNoFollow (ssh -> fs , & (sendCtx -> fp ), filePath ) != 0 ) {
2887+ #else
28422888 if (WFOPEN (ssh -> fs , & (sendCtx -> fp ), filePath , "rb" ) != 0 ) {
2889+ #endif
28432890 WLOG (WS_LOG_ERROR , "scp: Error with opening file, abort" );
28442891 wolfSSH_SetScpErrorMsg (ssh , "unable to open file "
28452892 "for reading" );
@@ -2862,7 +2909,10 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
28622909 }
28632910
28642911 } else {
2865- if (ret != WS_NEXT_ERROR ) {
2912+ /* WS_SCP_ABORT entries (e.g. a rejected symlink) were already logged at
2913+ * their source, so only the generic, unexpected-error case is noted
2914+ * here to avoid a misleading second log line. */
2915+ if (ret != WS_NEXT_ERROR && ret != WS_SCP_ABORT ) {
28662916 WLOG (WS_LOG_ERROR , "scp: ret does not equal WS_NEXT_ERROR, abort" );
28672917 ret = WS_SCP_ABORT ;
28682918 }
@@ -2948,6 +2998,22 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
29482998 * is complete.
29492999 * WS_SCP_ABORT - abort file transfer request
29503000 * WS_BAD_FILE_E - local file open error hit
3001+ *
3002+ * Symlink handling: file-content opens go through wFopenNoFollow and directory
3003+ * opens (both the recursive root and every descend) go through wOpendirNoFollow.
3004+ * Both are atomic against a swapped link on POSIX (O_NOFOLLOW, plus O_DIRECTORY
3005+ * for the directory open) and fall back to a wIsSymlink check-then-open on
3006+ * Windows or where O_NOFOLLOW is absent. The root also gets an explicit
3007+ * wIsSymlink pre-check because a trailing separator (open("link/", O_NOFOLLOW))
3008+ * can still follow the link; symlinks below the root are rejected as
3009+ * ScpProcessEntry traverses them. No "stays under a trusted base" containment
3010+ * is attempted: SCP has no library-level base path (wolfsshd relies on OS
3011+ * chroot) and wolfSSH_RealPath does not resolve links. GetFileStats uses WLSTAT
3012+ * so it does not follow a link for metadata or classification. On the
3013+ * Windows/fallback path the open is check-then-open, so a concurrent in-jail
3014+ * writer racing it remains a best-effort gap. For hostile multi-tenant use,
3015+ * confine the session with an OS mechanism (chroot, dropped privileges) and
3016+ * treat these checks as defense in depth.
29513017 */
29523018int wsScpSendCallback (WOLFSSH * ssh , int state , const char * peerRequest ,
29533019 char * fileName , word32 fileNameSz , word64 * mTime , word64 * aTime ,
@@ -2981,9 +3047,14 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
29813047 break ;
29823048
29833049 case WOLFSSH_SCP_SINGLE_FILE_REQUEST :
2984- if ((sendCtx == NULL ) || WFOPEN (ssh -> fs , & (sendCtx -> fp ), peerRequest ,
2985- "rb" ) != 0 ) {
2986-
3050+ /* open without following a symlink so its target is not streamed
3051+ * to the peer; see this function's symlink-handling note. */
3052+ if ((sendCtx == NULL ) ||
3053+ #ifdef WOLFSSH_HAVE_SYMLINK
3054+ wFopenNoFollow (ssh -> fs , & (sendCtx -> fp ), peerRequest ) != 0 ) {
3055+ #else
3056+ WFOPEN (ssh -> fs , & (sendCtx -> fp ), peerRequest , "rb" ) != 0 ) {
3057+ #endif
29873058 WLOG (WS_LOG_ERROR , "scp: unable to open file, abort" );
29883059 wolfSSH_SetScpErrorMsg (ssh , "unable to open file for reading" );
29893060 ret = WS_BAD_FILE_E ;
@@ -3037,9 +3108,51 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
30373108 case WOLFSSH_SCP_RECURSIVE_REQUEST :
30383109
30393110 if (ScpDirStackIsEmpty (sendCtx )) {
3111+ #ifdef WOLFSSH_HAVE_SYMLINK
3112+ word32 rootLen ;
3113+ #endif
3114+
3115+ /* first request, keep track of request directory. Reject a
3116+ * symlink root here (a trailing separator can still follow);
3117+ * see the symlink-handling note in this function's header. */
3118+ ret = WS_SUCCESS ;
3119+ if (peerRequest == NULL ) {
3120+ WLOG (WS_LOG_ERROR ,
3121+ "scp: missing recursive root path, abort" );
3122+ ret = WS_SCP_ABORT ;
3123+ }
3124+ #ifdef WOLFSSH_HAVE_SYMLINK
3125+ /* lstat() follows the link when the path ends in a separator,
3126+ * so check the root with any trailing separators removed */
3127+ else {
3128+ rootLen = (word32 )WSTRLEN (peerRequest );
3129+ while (rootLen > 1 && (peerRequest [rootLen - 1 ] == '/' ||
3130+ peerRequest [rootLen - 1 ] == '\\' ))
3131+ rootLen -- ;
3132+ if (rootLen >= DEFAULT_SCP_FILE_NAME_SZ ) {
3133+ WLOG (WS_LOG_ERROR ,
3134+ "scp: recursive root path too long, abort" );
3135+ wolfSSH_SetScpErrorMsg (ssh ,
3136+ "unable to open file for reading" );
3137+ ret = WS_SCP_ABORT ;
3138+ }
3139+ else {
3140+ WMEMCPY (filePath , peerRequest , rootLen );
3141+ filePath [rootLen ] = '\0' ;
3142+ if (wIsSymlink (filePath )) {
3143+ WLOG (WS_LOG_ERROR ,
3144+ "scp: refusing recursive root symlink, abort" );
3145+ wolfSSH_SetScpErrorMsg (ssh ,
3146+ "unable to open file for reading" );
3147+ ret = WS_SCP_ABORT ;
3148+ }
3149+ }
3150+ }
3151+ #endif /* WOLFSSH_HAVE_SYMLINK */
30403152
3041- /* first request, keep track of request directory */
3042- ret = ScpPushDir (ssh -> fs , sendCtx , peerRequest , ssh -> ctx -> heap );
3153+ if (ret == WS_SUCCESS )
3154+ ret = ScpPushDir (ssh -> fs , sendCtx , peerRequest ,
3155+ ssh -> ctx -> heap );
30433156
30443157 if (ret == WS_SUCCESS ) {
30453158 /* get file name from request */
@@ -3053,7 +3166,9 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
30533166
30543167 if (ret == WS_SUCCESS ) {
30553168 ret = WS_SCP_ENTER_DIR ;
3056- } else {
3169+ } else if (ret != WS_SCP_ABORT ) {
3170+ /* a rejected symlink root already logged its own reason;
3171+ * only note the generic stat failure here */
30573172 WLOG (WS_LOG_ERROR , "scp: error getting file stats, abort" );
30583173 ret = WS_SCP_ABORT ;
30593174 }
0 commit comments