From b0db2db98870acc7917329bc904ee1265e906bf6 Mon Sep 17 00:00:00 2001 From: drh <> Date: Wed, 7 Oct 2026 23:07:55 +0000 Subject: [PATCH] Improved escaping of filenames with unusual characters sent over the SSH link by the sqlite3_rsync utility program. ANT-2026-31X9HVRR. FossilOrigin-Name: eaec2f7f3744d4cdf0cdafce22e1909208f93734bf55263a042938919bd81310 --- manifest | 14 +-- manifest.uuid | 2 +- tool/sqlite3_rsync.c | 225 ++++++++++++++++++++++++++++--------------- 3 files changed, 156 insertions(+), 85 deletions(-) diff --git a/manifest b/manifest index 175985c65a..b191dc18df 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Mark\svec1\straining\sfunction\svec1_train()\sas\sSQLITE_DIRECTONLY,\son\sthe\sgrounds\sthat\sit\ssometimes\sprepares\sand\sexecutes\sSQL\sstatements\sbased\son\suser-supplied\sarguments. -D 2026-10-07T21:12:50.063 +C Improved\sescaping\sof\sfilenames\swith\sunusual\scharacters\ssent\sover\sthe\sSSH\nlink\sby\sthe\ssqlite3_rsync\sutility\sprogram.\s\sANT-2026-31X9HVRR. +D 2026-10-07T23:07:55.753 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -2261,7 +2261,7 @@ F tool/spellsift.tcl 52b4b04dc4333c7ab024f09d9d66ed6b6f7c6eb00b38497a09f338fa55d F tool/split-sqlite3c.tcl 4969fd642dad0ea483e4e104163021d92baf98f6a8eac981fe48525f9b873430 F tool/sqldiff.c c07b741de3fc629e30d7fcab9db04de94e94d7743ab38cadd778b2b1363f8d92 F tool/sqlite3_analyzer.c.in 14f02cb5ec3c264cd6107d1f1dad77092b1cf440fc196c30b69ae87b56a1a43b -F tool/sqlite3_rsync.c 160473120adc20ef1763d0b274cf618e83cb1aaf59dd479349a3daa5b7fb1e9d +F tool/sqlite3_rsync.c a342f9762653f968e12993bb2dc9083c52ad755c23f6005c575d8a376f970422 F tool/sqltclsh.c.in c103c6fc7d42bce611f9d4596774d60b7ef3d0b291a1f58c9e6184e458b89296 F tool/sqltclsh.tcl 862f4cf1418df5e1315b5db3b5ebe88969e2a784525af5fbf9596592f14ed848 F tool/src-verify.c 6c655d9a8d6b30f3648fc78a79bf3838ed68f8543869d380c43ea9f17b3b8501 @@ -2280,8 +2280,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P 2662a68f0d5ce1c8ffd6e8ec3e79b27f7cb5e6142a007a6641e1ac0b5a22037a -R 4db96a87f82315b023b2499afdf3d391 -U dan -Z ef9c919f90a321e41f9232bded41e49a +P 38c5972e914ba8c54b76f6de89501fce576c4a05a7f88f90c39b268203f54582 +R 87d911f0de84b405bcc492d75ba4f864 +U drh +Z 6d1b76a52e6bc82c7648e39f5983b795 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index cf04341a3a..ee9685e77f 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -38c5972e914ba8c54b76f6de89501fce576c4a05a7f88f90c39b268203f54582 +eaec2f7f3744d4cdf0cdafce22e1909208f93734bf55263a042938919bd81310 diff --git a/tool/sqlite3_rsync.c b/tool/sqlite3_rsync.c index d03c787b1d..b5cab04383 100644 --- a/tool/sqlite3_rsync.c +++ b/tool/sqlite3_rsync.c @@ -382,22 +382,7 @@ static int pclose2(FILE *pIn, FILE *pOut, int childPid){ ** 5 First byte of a 4-byte UTF-8 */ static const char aSafeChar[256] = { -#ifdef _WIN32 -/* Windows -** Prohibit: all control characters, including tab, \r and \n. -** Escape: (space) " # $ % & ' ( ) * ; < > ? [ ] ^ ` { | } -*/ -/* x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xa xb xc xd xe xf */ - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 0x */ - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 1x */ - 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, /* 2x */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, /* 3x */ - 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 4x */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, /* 5x */ - 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 6x */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, /* 7x */ -#else -/* Unix +/* ** Prohibit: all control characters, including tab, \r and \n ** Escape: (space) ! " # $ % & ' ( ) * ; < > ? [ \ ] ^ ` { | } */ @@ -410,7 +395,6 @@ static const char aSafeChar[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, /* 5x */ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 6x */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, /* 7x */ -#endif /* all bytes 0x80 through 0xbf are unescaped, being secondary ** bytes to UTF8 characters. Bytes 0xc0 through 0xff are the ** first byte of a UTF8 character and do get escaped */ @@ -424,52 +408,56 @@ static const char aSafeChar[256] = { 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 /* fx */ }; +/* +** Return true if the input contains special characters that need +** to be escaped. +*/ +int need_escape(const char *zIn){ + size_t i; + unsigned char c; + + /* Look for characters that need to be escaped */ + for(i=0; (c = (unsigned char)zIn[i])!=0; i++){ + if( aSafeChar[c] ) return 1; + } + return 0; +} + +/* +** Increment this global variable whenever the isFilename==2 hex encoding +** is used. +*/ +static int nHexEncode = 0; + /* ** pStr is a shell command under construction. This routine safely -** appends filename argument zIn. It returns 0 on success or non-zero -** on any error. +** appends filename argument zIn. ** ** The argument is escaped if it contains white space or other characters ** that need to be escaped for the shell. If zIn contains characters ** that cannot be safely escaped, then throw a fatal error. ** -** If the isFilename argument is true, then the argument is expected -** to be a filename. As shell commands commonly have command-line +** If the isFilename argument is 1 or 2, then the argument is expected +** to be a filename. isFilename==1 means to use shell-style escaping +** as the argument is going directly to the shell. isFilename==2 means +** to escape as hexadecimal (using A-P as the hex digits) instead of +** shell escaping. isFilename==2 argument are interpreted by another +** instance of sqlite3_rsync which knows how to decode them. +** +** For isFilename==1, because shell commands commonly have command-line ** options that begin with "-" and since we do not want an attacker ** to be able to invoke these switches using filenames that begin -** with "-", if zIn begins with "-", prepend an additional "./" -** (or ".\\" on Windows). +** with "-", if zIn begins with "-", prepend an additional "./". +** +** For isFilename==2, if the filename could be mistaken for a hexadecimal +** string (if it is an even number of characters and all characters are +** between A and P) then "./" is prepended so that the receiver +** will know that it is not an encoded filename. */ -int append_escaped_arg(sqlite3_str *pStr, const char *zIn, int isFilename){ - int i; - unsigned char c; - int needEscape = 0; +void append_escaped_arg(sqlite3_str *pStr, const char *zIn, int isFilename){ int n = sqlite3_str_length(pStr); char *z = sqlite3_str_value(pStr); - - /* Look for illegal byte-sequences and byte-sequences that require - ** escaping. No control-characters are allowed. All spaces and - ** non-ASCII unicode characters and some punctuation characters require - ** escaping. */ - for(i=0; (c = (unsigned char)zIn[i])!=0; i++){ - if( aSafeChar[c] ){ - unsigned char x = aSafeChar[c]; - needEscape = 1; - if( x==2 ){ - /* Bad ASCII character */ - return 1; - }else if( x>2 ){ - if( (zIn[i+1]&0xc0)!=0x80 - || (x>=4 && (zIn[i+2]&0xc0)!=0x80) - || (x==5 && (zIn[i+3]&0xc0)!=0x80) - ){ - /* Bad UTF8 character */ - return 1; - } - i += x-2; - } - } - } + size_t nIn = strlen(zIn); /* Separate from the previous argument by a space */ if( n>0 && !isspace(z[n-1]) ){ @@ -477,22 +465,31 @@ int append_escaped_arg(sqlite3_str *pStr, const char *zIn, int isFilename){ } /* Check for characters that need quoting */ - if( !needEscape ){ - if( isFilename && zIn[0]=='-' ){ + if( !need_escape(zIn) ){ + if( (isFilename>=1 && zIn[0]=='-') + || (isFilename==2 && (nIn&1)==0 && strspn(zIn,"ABCDEFGHIJKLMNOP")==nIn) + ){ sqlite3_str_appendchar(pStr, 1, '.'); -#if defined(_WIN32) - sqlite3_str_appendchar(pStr, 1, '\\'); -#else sqlite3_str_appendchar(pStr, 1, '/'); -#endif } sqlite3_str_appendall(pStr, zIn); + }else if( isFilename==2 ){ + /* Encode as hexadecimal using A-P as the hex digits. */ + size_t j; + for(j=0; j>4)&0xf)+'A'); + sqlite3_str_appendchar(pStr, 1, ((zIn[j])&0xf)+'A'); + } + nHexEncode++; }else{ + /* Use shell escapes */ #if defined(_WIN32) /* Quoting strategy for windows: ** Put the entire name inside of "...". Any " characters within ** the name get doubled. */ + size_t i; + unsigned char c; sqlite3_str_appendchar(pStr, 1, '"'); if( isFilename && zIn[0]=='-' ){ sqlite3_str_appendchar(pStr, 1, '.'); @@ -514,6 +511,8 @@ int append_escaped_arg(sqlite3_str *pStr, const char *zIn, int isFilename){ ** name, then put \ before each special character. */ if( strchr(zIn,'\'') ){ + unsigned char c; + size_t i; if( isFilename && zIn[0]=='-' ){ sqlite3_str_appendchar(pStr, 1, '.'); sqlite3_str_appendchar(pStr, 1, '/'); @@ -535,7 +534,6 @@ int append_escaped_arg(sqlite3_str *pStr, const char *zIn, int isFilename){ } #endif } - return 0; } /* Add an approprate PATH= argument to the SSH command under construction @@ -585,6 +583,36 @@ void add_path_argument(sqlite3_str *pStr){ ":/opt/local/bin:$PATH", 0); } +/* +** The input is a filename, possibly encoded as hexadecimal using +** characters 'A' through 'P' as the hexadecimal digits (instead of +** the usual '0' through 'F'). +** +** Return a copy of that filename, decoded if necessary. +*/ +static char *decode_filename(const char *zIn){ + size_t n; + if( zIn==0 ) return 0; + n = strlen(zIn); + if( (n&1)==1 || strspn(zIn,"ABCDEFGHIJKLMNOP")!=n ){ + /* Name is literal. No decoding required. */ + return sqlite3_mprintf("%s", zIn); + }else{ + /* Name is hex encoded. Need to decode it. */ + char *zOut = sqlite3_malloc64(n/2 + 1); + size_t i; + if( zOut==0 ){ + fprintf(stderr, "out of memory\n"); + exit(1); + } + for(i=0; i0 ){ + sqlite3_str_appendf(pStr, " --encoded-filenames %d", nHexEncode); + } if( ctx.eVerbose<2 && iRetry==0 ){ append_escaped_arg(pStr, "2>/dev/null", 0); } @@ -2331,17 +2396,20 @@ int main(int argc, char const * const *argv){ } if( zRemoteErrFile ){ append_escaped_arg(pStr, "--errorfile", 0); - append_escaped_arg(pStr, zRemoteErrFile, 1); + append_escaped_arg(pStr, zRemoteErrFile, 2); } if( zRemoteDebugFile ){ append_escaped_arg(pStr, "--debugfile", 0); - append_escaped_arg(pStr, zRemoteDebugFile, 1); + append_escaped_arg(pStr, zRemoteDebugFile, 2); } if( ctx.bWalOnly ){ append_escaped_arg(pStr, "--wal-only", 0); } - append_escaped_arg(pStr, file_tail(ctx.zOrigin), 1); - append_escaped_arg(pStr, zDiv, 1); + append_escaped_arg(pStr, file_tail(ctx.zOrigin), 2); + append_escaped_arg(pStr, zDiv, 2); + if( nHexEncode>0 ){ + sqlite3_str_appendf(pStr, " --encoded-filenames %d", nHexEncode); + } if( ctx.eVerbose<2 && iRetry==0 ){ append_escaped_arg(pStr, "2>/dev/null", 0); } @@ -2370,14 +2438,17 @@ int main(int argc, char const * const *argv){ } if( zRemoteErrFile ){ append_escaped_arg(pStr, "--errorfile", 0); - append_escaped_arg(pStr, zRemoteErrFile, 1); + append_escaped_arg(pStr, zRemoteErrFile, 2); } if( zRemoteDebugFile ){ append_escaped_arg(pStr, "--debugfile", 0); - append_escaped_arg(pStr, zRemoteDebugFile, 1); + append_escaped_arg(pStr, zRemoteDebugFile, 2); + } + append_escaped_arg(pStr, ctx.zOrigin, 2); + append_escaped_arg(pStr, ctx.zReplica, 2); + if( nHexEncode>0 ){ + sqlite3_str_appendf(pStr, " --encoded-filenames %d", nHexEncode); } - append_escaped_arg(pStr, ctx.zOrigin, 1); - append_escaped_arg(pStr, ctx.zReplica, 1); zCmd = sqlite3_str_finish(pStr); if( ctx.eVerbose>=2 ) printf("%s\n", zCmd); if( popen2(zCmd, &ctx.pIn, &ctx.pOut, &childPid, 0) ){