@@ -1816,6 +1816,22 @@ static int SFTP_IsSymlink(const char* path)
18161816#endif /* WOLFSSH_HAVE_SYMLINK */
18171817
18181818
1819+ /* Length of the confining prefix of an SFTP default path: its length with any
1820+ * trailing separators stripped (a lone "/" is kept). A result > 1 means the
1821+ * session is confined to something deeper than the filesystem root. Defined
1822+ * once here so the confinement gate stays consistent between GetAndCleanPath
1823+ * and SFTP_SessionConfined. Caller must pass a non-NULL path. */
1824+ static word32 SFTP_ConfinedPathLen (const char * defaultPath )
1825+ {
1826+ word32 dpLen = (word32 )WSTRLEN (defaultPath );
1827+
1828+ while (dpLen > 1 && WOLFSSH_SFTP_IS_DELIM (defaultPath [dpLen - 1 ])) {
1829+ dpLen -- ;
1830+ }
1831+ return dpLen ;
1832+ }
1833+
1834+
18191835/*
18201836 * This is a wrapper around the function wolfSSH_RealPath. Since it modifies
18211837 * the source path value, copy the path from the data stream into a local
@@ -1845,11 +1861,7 @@ static int GetAndCleanPath(const char* defaultPath,
18451861 /* defaultPath is stored in canonical form by
18461862 * wolfSSH_SFTP_SetDefaultPath, so a direct prefix compare against the
18471863 * canonical resolved request path enforces confinement. */
1848- dpLen = (word32 )WSTRLEN (defaultPath );
1849- /* strip trailing separator(s), but keep a lone "/" as-is */
1850- while (dpLen > 1 && WOLFSSH_SFTP_IS_DELIM (defaultPath [dpLen - 1 ])) {
1851- dpLen -- ;
1852- }
1864+ dpLen = SFTP_ConfinedPathLen (defaultPath );
18531865 if (dpLen > 1 ) {
18541866 /* resolved path must equal the default path or be within its
18551867 * subtree. On Windows the filesystem is case-insensitive and the
@@ -1911,6 +1923,22 @@ static int GetAndCleanPath(const char* defaultPath,
19111923}
19121924
19131925
1926+ #ifndef USE_WINDOWS_API
1927+ /* Returns 1 when the session is confined to a default path deeper than the
1928+ * filesystem root. This mirrors the gate GetAndCleanPath uses for its
1929+ * per-component symlink check, so the open-time symlink defenses stay limited
1930+ * to confined sessions and do not change behavior for servers that
1931+ * intentionally follow symlinks (the default, like OpenSSH's sftp-server). */
1932+ static int SFTP_SessionConfined (const char * defaultPath )
1933+ {
1934+ if (defaultPath == NULL ) {
1935+ return 0 ;
1936+ }
1937+ return (SFTP_ConfinedPathLen (defaultPath ) > 1 ) ? 1 : 0 ;
1938+ }
1939+ #endif /* !USE_WINDOWS_API */
1940+
1941+
19141942/* Builds a status packet and queues it for send.
19151943 * Returns WS_SUCCESS on success; ssh takes ownership of the allocated buffer. */
19161944static int SFTP_SendStatus (WOLFSSH * ssh , byte type , int reqId , const char * msg )
@@ -2271,6 +2299,7 @@ int wolfSSH_SFTP_RecvOpen(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz)
22712299 int rc ;
22722300 int fdOpened = 0 ;
22732301 int outOwnedBySsh = 0 ;
2302+ int confined ;
22742303
22752304 word32 outSz = sizeof (WFD ) + UINT32_SZ + WOLFSSH_SFTP_HEADER ;
22762305 byte * out = NULL ;
@@ -2290,6 +2319,10 @@ int wolfSSH_SFTP_RecvOpen(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz)
22902319
22912320 WLOG (WS_LOG_SFTP , "Receiving WOLFSSH_FTP_OPEN" );
22922321
2322+ /* only apply the symlink-follow defenses when the session is confined, so
2323+ * unconfined servers keep following symlinks as they did before */
2324+ confined = SFTP_SessionConfined (ssh -> sftpDefaultPath );
2325+
22932326 #if defined(MICROCHIP_MPLAB_HARMONY ) || defined(FREESCALE_MQX )
22942327 fd = WBADFILE ;
22952328 #else
@@ -2369,7 +2402,8 @@ int wolfSSH_SFTP_RecvOpen(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz)
23692402 WS_SFTP_FILEATRB fileAtr ;
23702403 WMEMSET (& fileAtr , 0 , sizeof (fileAtr ));
23712404 if (SFTP_GetAttributes (ssh -> fs ,
2372- dir , & fileAtr , 0 , ssh -> ctx -> heap ) == WS_SUCCESS ) {
2405+ dir , & fileAtr , (byte )confined , ssh -> ctx -> heap )
2406+ == WS_SUCCESS ) {
23732407 if ((fileAtr .per & FILEATRB_PER_MASK_TYPE )
23742408 != FILEATRB_PER_FILE ) {
23752409 ssh -> error = WS_SFTP_NOT_FILE_E ;
@@ -2391,6 +2425,13 @@ int wolfSSH_SFTP_RecvOpen(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz)
23912425 atr .per = 0644 ;
23922426 }
23932427
2428+ /* when confined, refuse to follow a symlink leaf, closing the TOCTOU
2429+ * window between the type check above and the open below. No-op where
2430+ * the platform has no O_NOFOLLOW (WOLFSSH_O_NOFOLLOW is 0). */
2431+ if (confined ) {
2432+ m |= WOLFSSH_O_NOFOLLOW ;
2433+ }
2434+
23942435 #ifdef MICROCHIP_MPLAB_HARMONY
23952436 {
23962437 WFILE * f = & fd ;
0 commit comments