Skip to content

Commit 09f2a18

Browse files
committed
sftp: refuse dir REMOVE and replacing RENAME
SSH_FXP_REMOVE does not take directories and SSH_FXP_RENAME fails on an existing newpath (draft-ietf-secsh-filexfer-02 6.5). The non-Windows handlers now check the target with an lstat first, and REMOVE unlinks. A regular file renames by linkat() and unlink(), which refuses an existing name atomically; anything else checks then calls rename(). - configure: probe for linkat() - port.h: WUNLINK on POSIX, WLINK as linkat() when configure finds it - FatFs, Nucleus: only "/" and "/." stand in for the root - regress: test both against files, directories, symlinks and a read-only directory Issue: F-15535, F-15536
1 parent 8455528 commit 09f2a18

4 files changed

Lines changed: 255 additions & 7 deletions

File tree

‎configure.ac‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@ AC_LINK_IFELSE(
148148
AC_MSG_RESULT([yes])],
149149
[AC_MSG_RESULT([no])])
150150

151+
AC_MSG_CHECKING([for linkat])
152+
AC_LINK_IFELSE(
153+
[AC_LANG_PROGRAM([[#include <fcntl.h>
154+
#include <unistd.h>]],
155+
[[(void)linkat(AT_FDCWD, "x", AT_FDCWD, "y", 0);]])],
156+
[AC_DEFINE([HAVE_LINKAT], [1], [linkat() available and declared])
157+
AC_MSG_RESULT([yes])],
158+
[AC_MSG_RESULT([no])])
159+
151160
AC_CHECK_DECLS([[pread],[pwrite]],,[unistd.h])
152161

153162
# DEBUG

‎src/wolfsftp.c‎

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4981,6 +4981,9 @@ int wolfSSH_SFTP_RecvRemove(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz)
49814981
char name[WOLFSSH_MAX_FILENAME];
49824982
word32 idx = 0;
49834983
int ret = WS_SUCCESS;
4984+
#ifndef USE_WINDOWS_API
4985+
WS_SFTP_FILEATRB atr;
4986+
#endif
49844987

49854988
byte type = WOLFSSH_FTP_OK;
49864989
int rc;
@@ -5006,7 +5009,20 @@ int wolfSSH_SFTP_RecvRemove(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz)
50065009

50075010
if (ret == WS_SUCCESS) {
50085011
#ifndef USE_WINDOWS_API
5009-
if (WREMOVE(ssh->fs, name) < 0)
5012+
/* REMOVE never takes a directory (draft-ietf-secsh-filexfer-02 6.5) */
5013+
WMEMSET(&atr, 0, sizeof(atr));
5014+
if (SFTP_GetAttributes(ssh->fs, name, &atr, 1, ssh->ctx->heap)
5015+
== WS_SUCCESS
5016+
&& (atr.per & FILEATRB_PER_MASK_TYPE) == FILEATRB_PER_DIR) {
5017+
WLOG(WS_LOG_SFTP, "Not removing a directory");
5018+
ret = WS_BAD_FILE_E;
5019+
}
5020+
#ifdef WUNLINK
5021+
/* unlink() refuses a directory swapped in after the check */
5022+
else if (WUNLINK(ssh->fs, name) < 0)
5023+
#else
5024+
else if (WREMOVE(ssh->fs, name) < 0)
5025+
#endif
50105026
#else /* USE_WINDOWS_API */
50115027
if (WS_DeleteFileA(name, ssh->ctx->heap) == 0)
50125028
#endif /* USE_WINDOWS_API */
@@ -5050,6 +5066,52 @@ int wolfSSH_SFTP_RecvRemove(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz)
50505066
}
50515067

50525068

5069+
#ifndef USE_WINDOWS_API
5070+
/* Rename without replacing an existing name (draft-ietf-secsh-filexfer-02
5071+
* 6.5). linkat() refuses one atomically; other types, builds without linkat(),
5072+
* and filesystems without hard links check first and race a concurrent
5073+
* create.
5074+
*
5075+
* returns WS_SUCCESS on success
5076+
*/
5077+
static int SFTP_RenameNoReplace(WOLFSSH* ssh, char* old, char* name)
5078+
{
5079+
WS_SFTP_FILEATRB atr;
5080+
5081+
#ifdef WLINK
5082+
WMEMSET(&atr, 0, sizeof(atr));
5083+
if (SFTP_GetAttributes(ssh->fs, old, &atr, 1, ssh->ctx->heap)
5084+
== WS_SUCCESS
5085+
&& (atr.per & FILEATRB_PER_MASK_TYPE) == FILEATRB_PER_FILE) {
5086+
if (WLINK(ssh->fs, old, name) == 0) {
5087+
if (WUNLINK(ssh->fs, old) == 0) {
5088+
return WS_SUCCESS;
5089+
}
5090+
/* undo the link */
5091+
(void)WUNLINK(ssh->fs, name);
5092+
return WS_BAD_FILE_E;
5093+
}
5094+
if (errno == EEXIST) {
5095+
WLOG(WS_LOG_SFTP, "Rename target exists");
5096+
return WS_BAD_FILE_E;
5097+
}
5098+
}
5099+
#endif
5100+
5101+
WMEMSET(&atr, 0, sizeof(atr));
5102+
if (SFTP_GetAttributes(ssh->fs, name, &atr, 1, ssh->ctx->heap)
5103+
== WS_SUCCESS) {
5104+
WLOG(WS_LOG_SFTP, "Rename target exists");
5105+
return WS_BAD_FILE_E;
5106+
}
5107+
if (WRENAME(ssh->fs, old, name) < 0) {
5108+
return WS_BAD_FILE_E;
5109+
}
5110+
return WS_SUCCESS;
5111+
}
5112+
#endif /* USE_WINDOWS_API */
5113+
5114+
50535115
/* Handles packet to rename a file
50545116
*
50555117
* returns WS_SUCCESS on success
@@ -5098,8 +5160,9 @@ int wolfSSH_SFTP_RecvRename(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz)
50985160

50995161
if (ret == WS_SUCCESS) {
51005162
#ifndef USE_WINDOWS_API
5101-
if (WRENAME(ssh->fs, old, name) < 0)
5163+
if (SFTP_RenameNoReplace(ssh, old, name) != WS_SUCCESS)
51025164
#else /* USE_WINDOWS_API */
5165+
/* MoveFile never replaces */
51035166
if (WS_MoveFileA(old, name, ssh->ctx->heap) == 0)
51045167
#endif /* USE_WINDOWS_API */
51055168
{
@@ -5212,8 +5275,10 @@ static int SFTP_GetAttributes(void* fs, const char* fileName,
52125275
return WS_SUCCESS;
52135276
}
52145277

5215-
/* handle case of "/" */
5216-
if (sz < 3 && fileName[0] == WS_DELIM && ret == NUF_NOFILE) {
5278+
/* handle case of "/" or "/.", not a root-level name such as "/a" */
5279+
if (fileName[0] == WS_DELIM
5280+
&& (sz == 1 || (sz == 2 && fileName[1] == '.'))
5281+
&& ret == NUF_NOFILE) {
52175282
atr->flags |= WOLFSSH_FILEATRB_PERM;
52185283
atr->per |= 040000;
52195284
return WS_SUCCESS;
@@ -5597,8 +5662,10 @@ static int SFTP_GetAttributes(void* fs, const char* fileName,
55975662
}
55985663

55995664
/* handle case of "/" */
5600-
/* Calling f_stat for "/" returns FR_INVALID_NAME. So we simulate the result. */
5601-
if (sz < 3 && fileName[0] == WS_DELIM) {
5665+
/* Calling f_stat for "/" returns FR_INVALID_NAME. So we simulate the result.
5666+
* Only "/" and "/." though; "/a" is a root-level name for f_stat. */
5667+
if (fileName[0] == WS_DELIM
5668+
&& (sz == 1 || (sz == 2 && fileName[1] == '.'))) {
56025669
atr->flags |= WOLFSSH_FILEATRB_PERM;
56035670
atr->per |= 040000;
56045671
return WS_SUCCESS;

‎tests/regress.c‎

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16021,6 +16021,167 @@ static void TestSftpAppendWritesAtEof(void)
1602116021
wolfSSH_free(ssh);
1602216022
wolfSSH_CTX_free(ctx);
1602316023
}
16024+
16025+
/* FXP_RENAME path to newPath, or FXP_REMOVE path when newPath is NULL. */
16026+
static int SftpPathRequest(WOLFSSH* ssh, int reqId, const char* path,
16027+
const char* newPath)
16028+
{
16029+
byte pkt[2 * (WOLFSSH_MAX_FILENAME + UINT32_SZ)];
16030+
word32 idx = 0;
16031+
word32 sz = (word32)WSTRLEN(path);
16032+
16033+
SftpPutU32(sz, pkt + idx); idx += UINT32_SZ;
16034+
WMEMCPY(pkt + idx, path, sz); idx += sz;
16035+
if (newPath == NULL) {
16036+
return wolfSSH_SFTP_RecvRemove(ssh, reqId, pkt, idx);
16037+
}
16038+
sz = (word32)WSTRLEN(newPath);
16039+
SftpPutU32(sz, pkt + idx); idx += UINT32_SZ;
16040+
WMEMCPY(pkt + idx, newPath, sz); idx += sz;
16041+
return wolfSSH_SFTP_RecvRename(ssh, reqId, pkt, idx);
16042+
}
16043+
16044+
static void SftpWriteTestFile(const char* path, const char* content)
16045+
{
16046+
WFILE* file;
16047+
16048+
AssertIntEQ(WFOPEN(NULL, &file, path, "wb"), 0);
16049+
AssertTrue(file != WBADFILE);
16050+
AssertIntEQ((int)WFWRITE(NULL, content, 1, WSTRLEN(content), file),
16051+
(int)WSTRLEN(content));
16052+
WFCLOSE(NULL, file);
16053+
}
16054+
16055+
/* REMOVE refuses a directory, RENAME an existing newpath
16056+
* (draft-ietf-secsh-filexfer-02 6.5). */
16057+
static void TestSftpRemoveRenameSemantics(void)
16058+
{
16059+
WOLFSSH_CTX* ctx;
16060+
WOLFSSH* ssh;
16061+
int rid = 700;
16062+
WSTAT_T st;
16063+
char cwd[WOLFSSH_MAX_FILENAME];
16064+
char fileA[64], fileB[64], fileC[64];
16065+
char dirA[64], dirB[64], dirC[64];
16066+
#if defined(WOLFSSH_HAVE_SYMLINK) && !defined(USE_WINDOWS_API)
16067+
char lnk[64];
16068+
#endif
16069+
#ifndef USE_WINDOWS_API
16070+
char roDir[64], roFile[128], roDest[128];
16071+
#endif
16072+
16073+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
16074+
AssertNotNull(ctx);
16075+
ssh = wolfSSH_new(ctx);
16076+
AssertNotNull(ssh);
16077+
AssertIntEQ(wolfSSH_SFTP_TestRecvStateInit(ssh), WS_SUCCESS);
16078+
16079+
WSNPRINTF(fileA, sizeof(fileA), "wolfssh_rm_a_%lu.tmp", SFTP_TEST_PID());
16080+
WSNPRINTF(fileB, sizeof(fileB), "wolfssh_rm_b_%lu.tmp", SFTP_TEST_PID());
16081+
WSNPRINTF(fileC, sizeof(fileC), "wolfssh_rm_c_%lu.tmp", SFTP_TEST_PID());
16082+
WSNPRINTF(dirA, sizeof(dirA), "wolfssh_rm_da_%lu", SFTP_TEST_PID());
16083+
WSNPRINTF(dirB, sizeof(dirB), "wolfssh_rm_db_%lu", SFTP_TEST_PID());
16084+
WSNPRINTF(dirC, sizeof(dirC), "wolfssh_rm_dc_%lu", SFTP_TEST_PID());
16085+
WMEMSET(cwd, 0, sizeof(cwd));
16086+
AssertNotNull(WGETCWD(ssh->fs, cwd, sizeof(cwd) - 1));
16087+
AssertIntEQ(wolfSSH_SFTP_SetDefaultPath(ssh, cwd), WS_SUCCESS);
16088+
16089+
SftpWriteTestFile(fileA, "aaaa");
16090+
SftpWriteTestFile(fileB, "bb");
16091+
(void)WREMOVE(ssh->fs, fileC);
16092+
AssertIntEQ(WMKDIR(ssh->fs, dirA, 0755), 0);
16093+
AssertIntEQ(WMKDIR(ssh->fs, dirB, 0755), 0);
16094+
(void)WRMDIR(ssh->fs, dirC);
16095+
16096+
/* REMOVE refuses an empty directory */
16097+
AssertIntEQ(SftpPathRequest(ssh, rid, dirA, NULL), WS_BAD_FILE_E);
16098+
AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_FAILURE);
16099+
AssertIntEQ(WSTAT(ssh->fs, dirA, &st), 0);
16100+
16101+
/* RENAME refuses an existing file; neither changes */
16102+
AssertIntEQ(SftpPathRequest(ssh, rid, fileA, fileB), WS_BAD_FILE_E);
16103+
AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_FAILURE);
16104+
AssertSftpFileContents(fileA, "aaaa", 4);
16105+
AssertSftpFileContents(fileB, "bb", 2);
16106+
16107+
/* RENAME refuses an existing empty directory */
16108+
AssertIntEQ(SftpPathRequest(ssh, rid, dirA, dirB), WS_BAD_FILE_E);
16109+
AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_FAILURE);
16110+
AssertIntEQ(WSTAT(ssh->fs, dirA, &st), 0);
16111+
AssertIntEQ(WSTAT(ssh->fs, dirB, &st), 0);
16112+
16113+
#if defined(WOLFSSH_HAVE_SYMLINK) && !defined(USE_WINDOWS_API)
16114+
/* a dangling symlink is an existing newpath too */
16115+
WSNPRINTF(lnk, sizeof(lnk), "wolfssh_rm_ln_%lu", SFTP_TEST_PID());
16116+
(void)WREMOVE(ssh->fs, lnk);
16117+
AssertIntEQ(symlink(fileC, lnk), 0);
16118+
AssertIntEQ(SftpPathRequest(ssh, rid, fileA, lnk), WS_BAD_FILE_E);
16119+
AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_FAILURE);
16120+
AssertIntEQ(WLSTAT(ssh->fs, lnk, &st), 0);
16121+
AssertTrue(S_ISLNK(st.st_mode));
16122+
AssertSftpFileContents(fileA, "aaaa", 4);
16123+
/* REMOVE takes the link, not its target */
16124+
AssertIntEQ(SftpPathRequest(ssh, rid, lnk, NULL), WS_SUCCESS);
16125+
AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK);
16126+
AssertTrue(WLSTAT(ssh->fs, lnk, &st) != 0);
16127+
/* a link to a directory is not a directory to REMOVE */
16128+
AssertIntEQ(symlink(dirB, lnk), 0);
16129+
AssertIntEQ(SftpPathRequest(ssh, rid, lnk, NULL), WS_SUCCESS);
16130+
AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK);
16131+
AssertTrue(WLSTAT(ssh->fs, lnk, &st) != 0);
16132+
AssertIntEQ(WSTAT(ssh->fs, dirB, &st), 0);
16133+
#endif
16134+
16135+
#ifndef USE_WINDOWS_API
16136+
/* a failed move leaves both names as they were: out of a read-only
16137+
* directory takes the link rollback, into one the link fallback */
16138+
if (geteuid() != 0) {
16139+
WSNPRINTF(roDir, sizeof(roDir), "wolfssh_rm_ro_%lu",
16140+
SFTP_TEST_PID());
16141+
WSNPRINTF(roFile, sizeof(roFile), "%s/%s", roDir, fileC);
16142+
WSNPRINTF(roDest, sizeof(roDest), "%s/%s", roDir, fileB);
16143+
AssertIntEQ(WMKDIR(ssh->fs, roDir, 0755), 0);
16144+
SftpWriteTestFile(roFile, "cc");
16145+
AssertIntEQ(chmod(roDir, 0555), 0);
16146+
16147+
AssertIntEQ(SftpPathRequest(ssh, rid, roFile, fileC), WS_BAD_FILE_E);
16148+
AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_FAILURE);
16149+
AssertSftpFileContents(roFile, "cc", 2);
16150+
AssertTrue(WLSTAT(ssh->fs, fileC, &st) != 0);
16151+
16152+
AssertIntEQ(SftpPathRequest(ssh, rid, fileB, roDest), WS_BAD_FILE_E);
16153+
AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_FAILURE);
16154+
AssertSftpFileContents(fileB, "bb", 2);
16155+
AssertTrue(WLSTAT(ssh->fs, roDest, &st) != 0);
16156+
16157+
AssertIntEQ(chmod(roDir, 0755), 0);
16158+
AssertIntEQ(WREMOVE(ssh->fs, roFile), 0);
16159+
AssertIntEQ(WRMDIR(ssh->fs, roDir), 0);
16160+
}
16161+
#endif
16162+
16163+
/* RENAME to a free name moves a file and a directory */
16164+
AssertIntEQ(SftpPathRequest(ssh, rid, fileA, fileC), WS_SUCCESS);
16165+
AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK);
16166+
AssertTrue(WSTAT(ssh->fs, fileA, &st) != 0);
16167+
AssertSftpFileContents(fileC, "aaaa", 4);
16168+
AssertIntEQ(SftpPathRequest(ssh, rid, dirA, dirC), WS_SUCCESS);
16169+
AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK);
16170+
AssertTrue(WSTAT(ssh->fs, dirA, &st) != 0);
16171+
AssertIntEQ(WSTAT(ssh->fs, dirC, &st), 0);
16172+
16173+
/* REMOVE still removes a file */
16174+
AssertIntEQ(SftpPathRequest(ssh, rid, fileC, NULL), WS_SUCCESS);
16175+
AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK);
16176+
AssertTrue(WSTAT(ssh->fs, fileC, &st) != 0);
16177+
16178+
(void)WREMOVE(ssh->fs, fileB);
16179+
(void)WRMDIR(ssh->fs, dirB);
16180+
(void)WRMDIR(ssh->fs, dirC);
16181+
wolfSSH_SFTP_TestRecvStateFree(ssh);
16182+
wolfSSH_free(ssh);
16183+
wolfSSH_CTX_free(ctx);
16184+
}
1602416185
#endif /* !NO_WOLFSSH_SERVER && !NO_FILESYSTEM */
1602516186

1602616187
#if !defined(NO_WOLFSSH_SERVER) && !defined(USE_WINDOWS_API) && \
@@ -20771,6 +20932,8 @@ int main(int argc, char** argv)
2077120932
#if !defined(NO_WOLFSSH_SERVER) && !defined(NO_FILESYSTEM)
2077220933
/* FXF_APPEND writes land at EOF regardless of the request offset */
2077320934
TestSftpAppendWritesAtEof();
20935+
/* REMOVE refuses directories, RENAME refuses an existing newpath */
20936+
TestSftpRemoveRenameSemantics();
2077420937
#endif
2077520938
#if !defined(NO_WOLFSSH_SERVER) && defined(USE_WINDOWS_API) && \
2077620939
!defined(NO_FILESYSTEM)

‎wolfssh/port.h‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1613,7 +1613,7 @@ extern "C" {
16131613

16141614
#else
16151615

1616-
#include <unistd.h> /* used for rmdir */
1616+
#include <unistd.h> /* used for rmdir, linkat, and unlink */
16171617
#include <sys/stat.h> /* used for mkdir, stat, and lstat */
16181618
#include <stdio.h> /* used for remove and rename */
16191619

@@ -1622,6 +1622,15 @@ extern "C" {
16221622
#define WSTAT(fs,p,b) stat((p),(b))
16231623
#ifndef USE_OSE_API
16241624
#define WLSTAT(fs,p,b) lstat((p),(b))
1625+
#define WUNLINK(fs,p) unlink((p))
1626+
#ifdef HAVE_LINKAT
1627+
/* SFTP no-replace rename; it reads errno. linkat() without
1628+
* AT_SYMLINK_FOLLOW links a symlink itself, where link() may
1629+
* follow it. */
1630+
#include <errno.h>
1631+
#include <fcntl.h>
1632+
#define WLINK(fs,o,n) linkat(AT_FDCWD, (o), AT_FDCWD, (n), 0)
1633+
#endif
16251634
#else
16261635
#define WLSTAT(fs,p,b) stat((p),(b))
16271636
#endif

0 commit comments

Comments
 (0)