@@ -2616,6 +2616,17 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
26162616 }
26172617 }
26182618
2619+ #ifdef WOLFSSH_HAVE_SYMLINK
2620+ /* GetFileStats classifies via WSTAT, which follows symlinks; a planted link
2621+ * to a file outside the traversed tree would be sent transparently. Reject
2622+ * the entry here, before descending into it or opening it, so neither the
2623+ * directory-descent nor the file-open sink below follows the link. */
2624+ if (ret == WS_SUCCESS && WS_IsSymlink (filePath )) {
2625+ WLOG (WS_LOG_ERROR , "scp: symlink entry rejected, aborting transfer" );
2626+ ret = WS_SCP_ABORT ;
2627+ }
2628+ #endif /* WOLFSSH_HAVE_SYMLINK */
2629+
26192630 if (ret == WS_SUCCESS ) {
26202631
26212632 if (ScpFileIsDir (sendCtx )) {
@@ -2652,7 +2663,10 @@ static int ScpProcessEntry(WOLFSSH* ssh, char* fileName, word64* mTime,
26522663 }
26532664
26542665 } else {
2655- if (ret != WS_NEXT_ERROR ) {
2666+ /* WS_SCP_ABORT entries (e.g. a rejected symlink) were already logged at
2667+ * their source, so only the generic, unexpected-error case is noted
2668+ * here to avoid a misleading second log line. */
2669+ if (ret != WS_NEXT_ERROR && ret != WS_SCP_ABORT ) {
26562670 WLOG (WS_LOG_ERROR , "scp: ret does not equal WS_NEXT_ERROR, abort" );
26572671 ret = WS_SCP_ABORT ;
26582672 }
@@ -2771,8 +2785,19 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
27712785 break ;
27722786
27732787 case WOLFSSH_SCP_SINGLE_FILE_REQUEST :
2774- if ((sendCtx == NULL ) || WFOPEN (ssh -> fs , & (sendCtx -> fp ), peerRequest ,
2775- "rb" ) != 0 ) {
2788+ #ifdef WOLFSSH_HAVE_SYMLINK
2789+ /* WFOPEN follows symlinks; refuse to open one so its target is not
2790+ * streamed to the peer. */
2791+ if (WS_IsSymlink (peerRequest )) {
2792+ WLOG (WS_LOG_ERROR , "scp: refusing to open symlink, abort" );
2793+ wolfSSH_SetScpErrorMsg (ssh , "unable to open file for reading" );
2794+ ret = WS_BAD_FILE_E ;
2795+ }
2796+ #endif /* WOLFSSH_HAVE_SYMLINK */
2797+
2798+ if (ret == WS_SUCCESS &&
2799+ ((sendCtx == NULL ) || WFOPEN (ssh -> fs , & (sendCtx -> fp ),
2800+ peerRequest , "rb" ) != 0 )) {
27762801
27772802 WLOG (WS_LOG_ERROR , "scp: unable to open file, abort" );
27782803 wolfSSH_SetScpErrorMsg (ssh , "unable to open file for reading" );
@@ -2828,7 +2853,17 @@ int wsScpSendCallback(WOLFSSH* ssh, int state, const char* peerRequest,
28282853
28292854 if (ScpDirStackIsEmpty (sendCtx )) {
28302855
2831- /* first request, keep track of request directory */
2856+ /* first request, keep track of request directory. The
2857+ * peer-named recursive root is intentionally not run through
2858+ * WS_IsSymlink here: unlike SFTP there is no library-level
2859+ * trusted base path to contain a resolved root against (SCP
2860+ * confinement in wolfsshd is enforced by OS chroot, inside
2861+ * which the kernel already prevents a symlink from escaping),
2862+ * and wolfSSH_RealPath does not resolve links so a "stays under
2863+ * the root" check is not possible. Symlinks discovered while
2864+ * traversing below the root are still rejected by
2865+ * ScpProcessEntry, which is the planted-link threat this guards
2866+ * against. */
28322867 ret = ScpPushDir (ssh -> fs , sendCtx , peerRequest , ssh -> ctx -> heap );
28332868
28342869 if (ret == WS_SUCCESS ) {
0 commit comments