@@ -2820,6 +2820,16 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
28202820 WSTRNCPY (fileName , sendCtx -> entry -> d_name ,
28212821 DEFAULT_SCP_FILE_NAME_SZ );
28222822 #endif
2823+ #ifdef WOLFSSH_HAVE_SYMLINK
2824+ /* filePath is fully built; reject a planted symlink before
2825+ * GetFileStats or any descend/open follows it. */
2826+ if (ret == WS_SUCCESS && wIsSymlink (filePath )) {
2827+ WLOG (WS_LOG_ERROR ,
2828+ "scp: symlink entry rejected, aborting transfer" );
2829+ ret = WS_SCP_ABORT ;
2830+ }
2831+ #endif /* WOLFSSH_HAVE_SYMLINK */
2832+
28232833 if (ret == WS_SUCCESS ) {
28242834 ret = GetFileStats (ssh -> fs , sendCtx , filePath , mTime , aTime , fileMode );
28252835 }
@@ -2839,7 +2849,11 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
28392849 }
28402850
28412851 } else if (ScpFileIsFile (sendCtx )) {
2852+ #ifdef WOLFSSH_HAVE_SYMLINK
2853+ if (wFopenNoFollow (& (sendCtx -> fp ), filePath ) != 0 ) {
2854+ #else
28422855 if (WFOPEN (ssh -> fs , & (sendCtx -> fp ), filePath , "rb" ) != 0 ) {
2856+ #endif
28432857 WLOG (WS_LOG_ERROR , "scp: Error with opening file, abort" );
28442858 wolfSSH_SetScpErrorMsg (ssh , "unable to open file "
28452859 "for reading" );
@@ -2862,7 +2876,10 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
28622876 }
28632877
28642878 } else {
2865- if (ret != WS_NEXT_ERROR ) {
2879+ /* WS_SCP_ABORT entries (e.g. a rejected symlink) were already logged at
2880+ * their source, so only the generic, unexpected-error case is noted
2881+ * here to avoid a misleading second log line. */
2882+ if (ret != WS_NEXT_ERROR && ret != WS_SCP_ABORT ) {
28662883 WLOG (WS_LOG_ERROR , "scp: ret does not equal WS_NEXT_ERROR, abort" );
28672884 ret = WS_SCP_ABORT ;
28682885 }
@@ -2948,6 +2965,20 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
29482965 * is complete.
29492966 * WS_SCP_ABORT - abort file transfer request
29502967 * WS_BAD_FILE_E - local file open error hit
2968+ *
2969+ * Symlink handling: file-content opens go through wFopenNoFollow, atomic
2970+ * against a swapped link on POSIX (O_NOFOLLOW) and falling back to a wIsSymlink
2971+ * check-then-open on Windows or where O_NOFOLLOW is absent. The recursive root
2972+ * is opened with opendir (which follows links), so it is leaf-checked with
2973+ * wIsSymlink instead; symlinks below the root are rejected as ScpProcessEntry
2974+ * traverses them. No "stays under a trusted base" containment is attempted:
2975+ * SCP has no library-level base path (wolfsshd relies on OS chroot) and
2976+ * wolfSSH_RealPath does not resolve links. The metadata WSTAT in GetFileStats
2977+ * still follows links and the Windows/fallback open is check-then-open, so
2978+ * those remain best-effort: a concurrent in-jail writer running as the server
2979+ * user could race the metadata stat. For hostile multi-tenant use, confine the
2980+ * session with an OS mechanism (chroot, dropped privileges) and treat these
2981+ * checks as defense in depth.
29512982 */
29522983int wsScpSendCallback (WOLFSSH * ssh , int state , const char * peerRequest ,
29532984 char * fileName , word32 fileNameSz , word64 * mTime , word64 * aTime ,
@@ -2981,9 +3012,14 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
29813012 break ;
29823013
29833014 case WOLFSSH_SCP_SINGLE_FILE_REQUEST :
2984- if ((sendCtx == NULL ) || WFOPEN (ssh -> fs , & (sendCtx -> fp ), peerRequest ,
2985- "rb" ) != 0 ) {
2986-
3015+ /* open without following a symlink so its target is not streamed
3016+ * to the peer; see this function's symlink-handling note. */
3017+ if ((sendCtx == NULL ) ||
3018+ #ifdef WOLFSSH_HAVE_SYMLINK
3019+ wFopenNoFollow (& (sendCtx -> fp ), peerRequest ) != 0 ) {
3020+ #else
3021+ WFOPEN (ssh -> fs , & (sendCtx -> fp ), peerRequest , "rb" ) != 0 ) {
3022+ #endif
29873023 WLOG (WS_LOG_ERROR , "scp: unable to open file, abort" );
29883024 wolfSSH_SetScpErrorMsg (ssh , "unable to open file for reading" );
29893025 ret = WS_BAD_FILE_E ;
@@ -3038,8 +3074,23 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
30383074
30393075 if (ScpDirStackIsEmpty (sendCtx )) {
30403076
3041- /* first request, keep track of request directory */
3042- ret = ScpPushDir (ssh -> fs , sendCtx , peerRequest , ssh -> ctx -> heap );
3077+ /* first request, keep track of request directory. Reject a
3078+ * symlink root here (opendir would follow it); see the
3079+ * symlink-handling note in this function's header. */
3080+ ret = WS_SUCCESS ;
3081+ #ifdef WOLFSSH_HAVE_SYMLINK
3082+ if (wIsSymlink (peerRequest )) {
3083+ WLOG (WS_LOG_ERROR ,
3084+ "scp: refusing recursive root symlink, abort" );
3085+ wolfSSH_SetScpErrorMsg (ssh ,
3086+ "unable to open file for reading" );
3087+ ret = WS_SCP_ABORT ;
3088+ }
3089+ #endif /* WOLFSSH_HAVE_SYMLINK */
3090+
3091+ if (ret == WS_SUCCESS )
3092+ ret = ScpPushDir (ssh -> fs , sendCtx , peerRequest ,
3093+ ssh -> ctx -> heap );
30433094
30443095 if (ret == WS_SUCCESS ) {
30453096 /* get file name from request */
@@ -3053,7 +3104,9 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
30533104
30543105 if (ret == WS_SUCCESS ) {
30553106 ret = WS_SCP_ENTER_DIR ;
3056- } else {
3107+ } else if (ret != WS_SCP_ABORT ) {
3108+ /* a rejected symlink root already logged its own reason;
3109+ * only note the generic stat failure here */
30573110 WLOG (WS_LOG_ERROR , "scp: error getting file stats, abort" );
30583111 ret = WS_SCP_ABORT ;
30593112 }
0 commit comments