@@ -2820,6 +2820,18 @@ 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 now fully built. Reject a planted symlink here,
2825+ * before GetFileStats (which classifies via WSTAT and follows
2826+ * links) or any later descend/open follows it - the whole point is
2827+ * to refuse the link before any file operation traverses it. */
2828+ if (ret == WS_SUCCESS && wIsSymlink (filePath )) {
2829+ WLOG (WS_LOG_ERROR ,
2830+ "scp: symlink entry rejected, aborting transfer" );
2831+ ret = WS_SCP_ABORT ;
2832+ }
2833+ #endif /* WOLFSSH_HAVE_SYMLINK */
2834+
28232835 if (ret == WS_SUCCESS ) {
28242836 ret = GetFileStats (ssh -> fs , sendCtx , filePath , mTime , aTime , fileMode );
28252837 }
@@ -2862,7 +2874,10 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
28622874 }
28632875
28642876 } else {
2865- if (ret != WS_NEXT_ERROR ) {
2877+ /* WS_SCP_ABORT entries (e.g. a rejected symlink) were already logged at
2878+ * their source, so only the generic, unexpected-error case is noted
2879+ * here to avoid a misleading second log line. */
2880+ if (ret != WS_NEXT_ERROR && ret != WS_SCP_ABORT ) {
28662881 WLOG (WS_LOG_ERROR , "scp: ret does not equal WS_NEXT_ERROR, abort" );
28672882 ret = WS_SCP_ABORT ;
28682883 }
@@ -2981,8 +2996,19 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
29812996 break ;
29822997
29832998 case WOLFSSH_SCP_SINGLE_FILE_REQUEST :
2984- if ((sendCtx == NULL ) || WFOPEN (ssh -> fs , & (sendCtx -> fp ), peerRequest ,
2985- "rb" ) != 0 ) {
2999+ #ifdef WOLFSSH_HAVE_SYMLINK
3000+ /* WFOPEN follows symlinks; refuse to open one so its target is not
3001+ * streamed to the peer. */
3002+ if (wIsSymlink (peerRequest )) {
3003+ WLOG (WS_LOG_ERROR , "scp: refusing to open symlink, abort" );
3004+ wolfSSH_SetScpErrorMsg (ssh , "unable to open file for reading" );
3005+ ret = WS_BAD_FILE_E ;
3006+ }
3007+ #endif /* WOLFSSH_HAVE_SYMLINK */
3008+
3009+ if (ret == WS_SUCCESS &&
3010+ ((sendCtx == NULL ) || WFOPEN (ssh -> fs , & (sendCtx -> fp ),
3011+ peerRequest , "rb" ) != 0 )) {
29863012
29873013 WLOG (WS_LOG_ERROR , "scp: unable to open file, abort" );
29883014 wolfSSH_SetScpErrorMsg (ssh , "unable to open file for reading" );
@@ -3038,7 +3064,17 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
30383064
30393065 if (ScpDirStackIsEmpty (sendCtx )) {
30403066
3041- /* first request, keep track of request directory */
3067+ /* first request, keep track of request directory. The
3068+ * peer-named recursive root is intentionally not run through
3069+ * wIsSymlink here: unlike SFTP there is no library-level
3070+ * trusted base path to contain a resolved root against (SCP
3071+ * confinement in wolfsshd is enforced by OS chroot, inside
3072+ * which the kernel already prevents a symlink from escaping),
3073+ * and wolfSSH_RealPath does not resolve links so a "stays under
3074+ * the root" check is not possible. Symlinks discovered while
3075+ * traversing below the root are still rejected by
3076+ * ScpProcessEntry, which is the planted-link threat this guards
3077+ * against. */
30423078 ret = ScpPushDir (ssh -> fs , sendCtx , peerRequest , ssh -> ctx -> heap );
30433079
30443080 if (ret == WS_SUCCESS ) {
0 commit comments