@@ -2610,6 +2610,18 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
26102610 WSTRNCPY (fileName , sendCtx -> entry -> d_name ,
26112611 DEFAULT_SCP_FILE_NAME_SZ );
26122612 #endif
2613+ #ifdef WOLFSSH_HAVE_SYMLINK
2614+ /* filePath is now fully built. Reject a planted symlink here,
2615+ * before GetFileStats (which classifies via WSTAT and follows
2616+ * links) or any later descend/open follows it - the whole point is
2617+ * to refuse the link before any file operation traverses it. */
2618+ if (ret == WS_SUCCESS && WS_IsSymlink (filePath )) {
2619+ WLOG (WS_LOG_ERROR ,
2620+ "scp: symlink entry rejected, aborting transfer" );
2621+ ret = WS_SCP_ABORT ;
2622+ }
2623+ #endif /* WOLFSSH_HAVE_SYMLINK */
2624+
26132625 if (ret == WS_SUCCESS ) {
26142626 ret = GetFileStats (ssh -> fs , sendCtx , filePath , mTime , aTime , fileMode );
26152627 }
@@ -2652,7 +2664,10 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
26522664 }
26532665
26542666 } else {
2655- if (ret != WS_NEXT_ERROR ) {
2667+ /* WS_SCP_ABORT entries (e.g. a rejected symlink) were already logged at
2668+ * their source, so only the generic, unexpected-error case is noted
2669+ * here to avoid a misleading second log line. */
2670+ if (ret != WS_NEXT_ERROR && ret != WS_SCP_ABORT ) {
26562671 WLOG (WS_LOG_ERROR , "scp: ret does not equal WS_NEXT_ERROR, abort" );
26572672 ret = WS_SCP_ABORT ;
26582673 }
@@ -2771,8 +2786,19 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
27712786 break ;
27722787
27732788 case WOLFSSH_SCP_SINGLE_FILE_REQUEST :
2774- if ((sendCtx == NULL ) || WFOPEN (ssh -> fs , & (sendCtx -> fp ), peerRequest ,
2775- "rb" ) != 0 ) {
2789+ #ifdef WOLFSSH_HAVE_SYMLINK
2790+ /* WFOPEN follows symlinks; refuse to open one so its target is not
2791+ * streamed to the peer. */
2792+ if (WS_IsSymlink (peerRequest )) {
2793+ WLOG (WS_LOG_ERROR , "scp: refusing to open symlink, abort" );
2794+ wolfSSH_SetScpErrorMsg (ssh , "unable to open file for reading" );
2795+ ret = WS_BAD_FILE_E ;
2796+ }
2797+ #endif /* WOLFSSH_HAVE_SYMLINK */
2798+
2799+ if (ret == WS_SUCCESS &&
2800+ ((sendCtx == NULL ) || WFOPEN (ssh -> fs , & (sendCtx -> fp ),
2801+ peerRequest , "rb" ) != 0 )) {
27762802
27772803 WLOG (WS_LOG_ERROR , "scp: unable to open file, abort" );
27782804 wolfSSH_SetScpErrorMsg (ssh , "unable to open file for reading" );
@@ -2828,7 +2854,17 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
28282854
28292855 if (ScpDirStackIsEmpty (sendCtx )) {
28302856
2831- /* first request, keep track of request directory */
2857+ /* first request, keep track of request directory. The
2858+ * peer-named recursive root is intentionally not run through
2859+ * WS_IsSymlink here: unlike SFTP there is no library-level
2860+ * trusted base path to contain a resolved root against (SCP
2861+ * confinement in wolfsshd is enforced by OS chroot, inside
2862+ * which the kernel already prevents a symlink from escaping),
2863+ * and wolfSSH_RealPath does not resolve links so a "stays under
2864+ * the root" check is not possible. Symlinks discovered while
2865+ * traversing below the root are still rejected by
2866+ * ScpProcessEntry, which is the planted-link threat this guards
2867+ * against. */
28322868 ret = ScpPushDir (ssh -> fs , sendCtx , peerRequest , ssh -> ctx -> heap );
28332869
28342870 if (ret == WS_SUCCESS ) {
0 commit comments