@@ -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 &&
@@ -2626,6 +2629,17 @@ int ScpPopDir(void *fs, ScpSendCtx* ctx, void* heap)
26262629 return WS_SUCCESS ;
26272630}
26282631
2632+ /* Drain dir-stack entries (and open dir handles) left on a send context after
2633+ * a recursive transfer aborts mid-tree before popping. Safe on an empty
2634+ * stack. */
2635+ void ScpSendCtxFreeDirs (void * fs , ScpSendCtx * ctx , void * heap )
2636+ {
2637+ if (ctx != NULL ) {
2638+ while (ctx -> currentDir != NULL )
2639+ (void )ScpPopDir (fs , ctx , heap );
2640+ }
2641+ }
2642+
26292643/* Get next entry in directory, either file or directory, skips self (.)
26302644 * and parent (..) directories, stores in ctx->entry.
26312645 * Return WS_SUCCESS on success or negative upon error */
@@ -2820,6 +2834,16 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
28202834 WSTRNCPY (fileName , sendCtx -> entry -> d_name ,
28212835 DEFAULT_SCP_FILE_NAME_SZ );
28222836 #endif
2837+ #ifdef WOLFSSH_HAVE_SYMLINK
2838+ /* filePath is fully built; reject a planted symlink before
2839+ * GetFileStats or any descend/open follows it. */
2840+ if (ret == WS_SUCCESS && wIsSymlink (filePath )) {
2841+ WLOG (WS_LOG_ERROR ,
2842+ "scp: symlink entry rejected, aborting transfer" );
2843+ ret = WS_SCP_ABORT ;
2844+ }
2845+ #endif /* WOLFSSH_HAVE_SYMLINK */
2846+
28232847 if (ret == WS_SUCCESS ) {
28242848 ret = GetFileStats (ssh -> fs , sendCtx , filePath , mTime , aTime , fileMode );
28252849 }
@@ -2839,7 +2863,11 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
28392863 }
28402864
28412865 } else if (ScpFileIsFile (sendCtx )) {
2866+ #ifdef WOLFSSH_HAVE_SYMLINK
2867+ if (wFopenNoFollow (ssh -> fs , & (sendCtx -> fp ), filePath ) != 0 ) {
2868+ #else
28422869 if (WFOPEN (ssh -> fs , & (sendCtx -> fp ), filePath , "rb" ) != 0 ) {
2870+ #endif
28432871 WLOG (WS_LOG_ERROR , "scp: Error with opening file, abort" );
28442872 wolfSSH_SetScpErrorMsg (ssh , "unable to open file "
28452873 "for reading" );
@@ -2862,7 +2890,10 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
28622890 }
28632891
28642892 } else {
2865- if (ret != WS_NEXT_ERROR ) {
2893+ /* WS_SCP_ABORT entries (e.g. a rejected symlink) were already logged at
2894+ * their source, so only the generic, unexpected-error case is noted
2895+ * here to avoid a misleading second log line. */
2896+ if (ret != WS_NEXT_ERROR && ret != WS_SCP_ABORT ) {
28662897 WLOG (WS_LOG_ERROR , "scp: ret does not equal WS_NEXT_ERROR, abort" );
28672898 ret = WS_SCP_ABORT ;
28682899 }
@@ -2948,6 +2979,21 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
29482979 * is complete.
29492980 * WS_SCP_ABORT - abort file transfer request
29502981 * WS_BAD_FILE_E - local file open error hit
2982+ *
2983+ * Symlink handling: file-content opens go through wFopenNoFollow, atomic
2984+ * against a swapped link on POSIX (O_NOFOLLOW) and falling back to a wIsSymlink
2985+ * check-then-open on Windows or where O_NOFOLLOW is absent. The recursive root
2986+ * is opened with opendir (which follows links), so it is leaf-checked with
2987+ * wIsSymlink instead; symlinks below the root are rejected as ScpProcessEntry
2988+ * traverses them. No "stays under a trusted base" containment is attempted:
2989+ * SCP has no library-level base path (wolfsshd relies on OS chroot) and
2990+ * wolfSSH_RealPath does not resolve links. GetFileStats uses WLSTAT so it does
2991+ * not follow a link for metadata or classification, but the directory descend
2992+ * still opens with opendir (which follows), and the Windows/fallback open is
2993+ * check-then-open, so a concurrent in-jail writer racing the descend remains a
2994+ * best-effort gap. For hostile multi-tenant use, confine the session with an OS
2995+ * mechanism (chroot, dropped privileges) and treat these checks as defense in
2996+ * depth.
29512997 */
29522998int wsScpSendCallback (WOLFSSH * ssh , int state , const char * peerRequest ,
29532999 char * fileName , word32 fileNameSz , word64 * mTime , word64 * aTime ,
@@ -2981,9 +3027,14 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
29813027 break ;
29823028
29833029 case WOLFSSH_SCP_SINGLE_FILE_REQUEST :
2984- if ((sendCtx == NULL ) || WFOPEN (ssh -> fs , & (sendCtx -> fp ), peerRequest ,
2985- "rb" ) != 0 ) {
2986-
3030+ /* open without following a symlink so its target is not streamed
3031+ * to the peer; see this function's symlink-handling note. */
3032+ if ((sendCtx == NULL ) ||
3033+ #ifdef WOLFSSH_HAVE_SYMLINK
3034+ wFopenNoFollow (ssh -> fs , & (sendCtx -> fp ), peerRequest ) != 0 ) {
3035+ #else
3036+ WFOPEN (ssh -> fs , & (sendCtx -> fp ), peerRequest , "rb" ) != 0 ) {
3037+ #endif
29873038 WLOG (WS_LOG_ERROR , "scp: unable to open file, abort" );
29883039 wolfSSH_SetScpErrorMsg (ssh , "unable to open file for reading" );
29893040 ret = WS_BAD_FILE_E ;
@@ -3037,9 +3088,40 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
30373088 case WOLFSSH_SCP_RECURSIVE_REQUEST :
30383089
30393090 if (ScpDirStackIsEmpty (sendCtx )) {
3091+ #ifdef WOLFSSH_HAVE_SYMLINK
3092+ word32 rootLen ;
3093+ #endif
3094+
3095+ /* first request, keep track of request directory. Reject a
3096+ * symlink root here (opendir would follow it); see the
3097+ * symlink-handling note in this function's header. */
3098+ ret = WS_SUCCESS ;
3099+ #ifdef WOLFSSH_HAVE_SYMLINK
3100+ /* lstat() follows the link when the path ends in a separator,
3101+ * so check the root with any trailing separators removed */
3102+ rootLen = (word32 )WSTRLEN (peerRequest );
3103+ while (rootLen > 1 && (peerRequest [rootLen - 1 ] == '/' ||
3104+ peerRequest [rootLen - 1 ] == '\\' ))
3105+ rootLen -- ;
3106+ if (rootLen >= DEFAULT_SCP_FILE_NAME_SZ ) {
3107+ ret = WS_SCP_ABORT ;
3108+ }
3109+ else {
3110+ WMEMCPY (filePath , peerRequest , rootLen );
3111+ filePath [rootLen ] = '\0' ;
3112+ if (wIsSymlink (filePath )) {
3113+ WLOG (WS_LOG_ERROR ,
3114+ "scp: refusing recursive root symlink, abort" );
3115+ wolfSSH_SetScpErrorMsg (ssh ,
3116+ "unable to open file for reading" );
3117+ ret = WS_SCP_ABORT ;
3118+ }
3119+ }
3120+ #endif /* WOLFSSH_HAVE_SYMLINK */
30403121
3041- /* first request, keep track of request directory */
3042- ret = ScpPushDir (ssh -> fs , sendCtx , peerRequest , ssh -> ctx -> heap );
3122+ if (ret == WS_SUCCESS )
3123+ ret = ScpPushDir (ssh -> fs , sendCtx , peerRequest ,
3124+ ssh -> ctx -> heap );
30433125
30443126 if (ret == WS_SUCCESS ) {
30453127 /* get file name from request */
@@ -3053,7 +3135,9 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
30533135
30543136 if (ret == WS_SUCCESS ) {
30553137 ret = WS_SCP_ENTER_DIR ;
3056- } else {
3138+ } else if (ret != WS_SCP_ABORT ) {
3139+ /* a rejected symlink root already logged its own reason;
3140+ * only note the generic stat failure here */
30573141 WLOG (WS_LOG_ERROR , "scp: error getting file stats, abort" );
30583142 ret = WS_SCP_ABORT ;
30593143 }
0 commit comments