Skip to content

Commit 65d30a1

Browse files
committed
Merge branch 'win32-filenames-cannot-have-trailing-spaces-or-periods'
On Windows, filenames cannot have trailing spaces or periods, when opening such paths, they are stripped automatically. Read: you can open the file `README` via the file name `README . . .`. This ambiguity can be used in combination with other security bugs to cause e.g. remote code execution during recursive clones. This patch series fixes that. Signed-off-by: Johannes Schindelin <[email protected]>
2 parents 5532ebd + d2c84da commit 65d30a1

11 files changed

+127
-4
lines changed

compat/mingw.c

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,12 @@ int mingw_mkdir(const char *path, int mode)
333333
{
334334
int ret;
335335
wchar_t wpath[MAX_PATH];
336+
337+
if (!is_valid_win32_path(path)) {
338+
errno = EINVAL;
339+
return -1;
340+
}
341+
336342
if (xutftowcs_path(wpath, path) < 0)
337343
return -1;
338344
ret = _wmkdir(wpath);
@@ -345,13 +351,18 @@ int mingw_open (const char *filename, int oflags, ...)
345351
{
346352
va_list args;
347353
unsigned mode;
348-
int fd;
354+
int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
349355
wchar_t wfilename[MAX_PATH];
350356

351357
va_start(args, oflags);
352358
mode = va_arg(args, int);
353359
va_end(args);
354360

361+
if (!is_valid_win32_path(filename)) {
362+
errno = create ? EINVAL : ENOENT;
363+
return -1;
364+
}
365+
355366
if (filename && !strcmp(filename, "/dev/null"))
356367
filename = "nul";
357368

@@ -413,6 +424,11 @@ FILE *mingw_fopen (const char *filename, const char *otype)
413424
int hide = needs_hiding(filename);
414425
FILE *file;
415426
wchar_t wfilename[MAX_PATH], wotype[4];
427+
if (!is_valid_win32_path(filename)) {
428+
int create = otype && strchr(otype, 'w');
429+
errno = create ? EINVAL : ENOENT;
430+
return NULL;
431+
}
416432
if (filename && !strcmp(filename, "/dev/null"))
417433
filename = "nul";
418434
if (xutftowcs_path(wfilename, filename) < 0 ||
@@ -435,6 +451,11 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
435451
int hide = needs_hiding(filename);
436452
FILE *file;
437453
wchar_t wfilename[MAX_PATH], wotype[4];
454+
if (!is_valid_win32_path(filename)) {
455+
int create = otype && strchr(otype, 'w');
456+
errno = create ? EINVAL : ENOENT;
457+
return NULL;
458+
}
438459
if (filename && !strcmp(filename, "/dev/null"))
439460
filename = "nul";
440461
if (xutftowcs_path(wfilename, filename) < 0 ||
@@ -2109,6 +2130,40 @@ static void setup_windows_environment(void)
21092130
setenv("TERM", "cygwin", 1);
21102131
}
21112132

2133+
int is_valid_win32_path(const char *path)
2134+
{
2135+
int preceding_space_or_period = 0, i = 0, periods = 0;
2136+
2137+
if (!protect_ntfs)
2138+
return 1;
2139+
2140+
for (;;) {
2141+
char c = *(path++);
2142+
switch (c) {
2143+
case '\0':
2144+
case '/': case '\\':
2145+
/* cannot end in ` ` or `.`, except for `.` and `..` */
2146+
if (preceding_space_or_period &&
2147+
(i != periods || periods > 2))
2148+
return 0;
2149+
if (!c)
2150+
return 1;
2151+
2152+
i = periods = preceding_space_or_period = 0;
2153+
continue;
2154+
case '.':
2155+
periods++;
2156+
/* fallthru */
2157+
case ' ':
2158+
preceding_space_or_period = 1;
2159+
i++;
2160+
continue;
2161+
}
2162+
preceding_space_or_period = 0;
2163+
i++;
2164+
}
2165+
}
2166+
21122167
/*
21132168
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
21142169
* mingw startup code, see init.c in mingw runtime).

compat/mingw.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,17 @@ int mingw_offset_1st_component(const char *path);
428428
#include <inttypes.h>
429429
#endif
430430

431+
/**
432+
* Verifies that the given path is a valid one on Windows.
433+
*
434+
* In particular, path segments are disallowed which end in a period or a
435+
* space (except the special directories `.` and `..`).
436+
*
437+
* Returns 1 upon success, otherwise 0.
438+
*/
439+
int is_valid_win32_path(const char *path);
440+
#define is_valid_path(path) is_valid_win32_path(path)
441+
431442
/**
432443
* Converts UTF-8 encoded string to UTF-16LE.
433444
*

git-compat-util.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,10 @@ static inline int git_offset_1st_component(const char *path)
370370
#define offset_1st_component git_offset_1st_component
371371
#endif
372372

373+
#ifndef is_valid_path
374+
#define is_valid_path(path) 1
375+
#endif
376+
373377
#ifndef find_last_dir_sep
374378
static inline char *git_find_last_dir_sep(const char *path)
375379
{

read-cache.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,9 @@ int verify_path(const char *path, unsigned mode)
847847
if (has_dos_drive_prefix(path))
848848
return 0;
849849

850+
if (!is_valid_path(path))
851+
return 0;
852+
850853
goto inside;
851854
for (;;) {
852855
if (!c)

t/helper/test-path-utils.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,23 @@ int cmd_main(int argc, const char **argv)
386386
if (argc > 1 && !strcmp(argv[1], "protect_ntfs_hfs"))
387387
return !!protect_ntfs_hfs_benchmark(argc - 1, argv + 1);
388388

389+
if (argc > 1 && !strcmp(argv[1], "is_valid_path")) {
390+
int res = 0, expect = 1, i;
391+
392+
for (i = 2; i < argc; i++)
393+
if (!strcmp("--not", argv[i]))
394+
expect = 0;
395+
else if (expect != is_valid_path(argv[i]))
396+
res = error("'%s' is%s a valid path",
397+
argv[i], expect ? " not" : "");
398+
else
399+
fprintf(stderr,
400+
"'%s' is%s a valid path\n",
401+
argv[i], expect ? "" : " not");
402+
403+
return !!res;
404+
}
405+
389406
fprintf(stderr, "%s: unknown function name: %s\n", argv[0],
390407
argv[1] ? argv[1] : "(there was none)");
391408
return 1;

t/t0060-path-utils.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,4 +440,18 @@ test_expect_success 'match .gitmodules' '
440440
.gitmodules,:\$DATA
441441
'
442442

443+
test_expect_success MINGW 'is_valid_path() on Windows' '
444+
test-path-utils is_valid_path \
445+
win32 \
446+
"win32 x" \
447+
../hello.txt \
448+
\
449+
--not \
450+
"win32 " \
451+
"win32 /x " \
452+
"win32." \
453+
"win32 . ." \
454+
.../hello.txt
455+
'
456+
443457
test_done

t/t6130-pathspec-noglob.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ test_expect_success 'create commits with glob characters' '
1010
# the name "f*" in the worktree, because it is not allowed
1111
# on Windows (the tests below do not depend on the presence
1212
# of the file in the worktree)
13+
git config core.protectNTFS false &&
1314
git update-index --add --cacheinfo 100644 "$(git rev-parse HEAD:foo)" "f*" &&
1415
test_tick &&
1516
git commit -m star &&

t/t7415-submodule-names.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ test_expect_success MINGW 'prevent git~1 squatting on Windows' '
102102
) &&
103103
test_must_fail git -c core.protectNTFS=false \
104104
clone --recurse-submodules squatting squatting-clone 2>err &&
105-
test_i18ngrep "directory not empty" err &&
105+
test_i18ngrep -e "directory not empty" -e "not an empty directory" err &&
106106
! grep gitdir squatting-clone/d/a/git~2
107107
'
108108

t/t7417-submodule-path-url.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,21 @@ test_expect_success 'clone rejects unprotected dash' '
1717
test_i18ngrep ignoring err
1818
'
1919

20+
test_expect_success MINGW 'submodule paths disallows trailing spaces' '
21+
git init super &&
22+
test_must_fail git -C super submodule add ../upstream "sub " &&
23+
24+
: add "sub", then rename "sub" to "sub ", the hard way &&
25+
git -C super submodule add ../upstream sub &&
26+
tree=$(git -C super write-tree) &&
27+
git -C super ls-tree $tree >tree &&
28+
sed "s/sub/sub /" <tree >tree.new &&
29+
tree=$(git -C super mktree <tree.new) &&
30+
commit=$(echo with space | git -C super commit-tree $tree) &&
31+
git -C super update-ref refs/heads/master $commit &&
32+
33+
test_must_fail git clone --recurse-submodules super dst 2>err &&
34+
test_i18ngrep "sub " err
35+
'
36+
2037
test_done

t/t9350-fast-export.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ test_expect_success 'fast-export quotes pathnames' '
424424
test_config -C crazy-paths core.protectNTFS false &&
425425
(cd crazy-paths &&
426426
blob=$(echo foo | git hash-object -w --stdin) &&
427-
git update-index --add \
427+
git -c core.protectNTFS=false update-index --add \
428428
--cacheinfo 100644 $blob "$(printf "path with\\nnewline")" \
429429
--cacheinfo 100644 $blob "path with \"quote\"" \
430430
--cacheinfo 100644 $blob "path with \\backslash" \

0 commit comments

Comments
 (0)