Skip to content

Commit 397abe9

Browse files
committed
submodule: also validate Windows-separated paths for validity
Otherwise we would also admit `..\..\foo\bar` as a valid path and fail to protect Windows users. Ideally we would check for both separators without the need for the copied string, but this'll get us over the RCE.
1 parent 6b15cea commit 397abe9

File tree

2 files changed

+65
-15
lines changed

2 files changed

+65
-15
lines changed

src/submodule.c

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ static int load_submodule_names(git_strmap *out, git_repository *repo, git_confi
220220
git_config_iterator *iter;
221221
git_config_entry *entry;
222222
git_buf buf = GIT_BUF_INIT;
223-
int rval;
223+
int rval, isvalid;
224224
int error = 0;
225225

226226
if ((error = git_config_iterator_glob_new(&iter, cfg, key)) < 0)
@@ -232,7 +232,10 @@ static int load_submodule_names(git_strmap *out, git_repository *repo, git_confi
232232
ldot = strrchr(entry->name, '.');
233233

234234
git_buf_put(&buf, fdot + 1, ldot - fdot - 1);
235-
if (!git_submodule_name_is_valid(repo, buf.ptr, 0))
235+
isvalid = git_submodule_name_is_valid(repo, buf.ptr, 0);
236+
if (isvalid < 0)
237+
return isvalid;
238+
if (!isvalid)
236239
continue;
237240

238241
git_strmap_insert(out, entry->value, git_buf_detach(&buf), &rval);
@@ -364,11 +367,25 @@ int git_submodule_lookup(
364367

365368
int git_submodule_name_is_valid(const git_repository *repo, const char *name, int flags)
366369
{
370+
git_buf buf = GIT_BUF_INIT;
371+
int error, isvalid;
372+
367373
if (flags == 0)
368374
flags = GIT_PATH_REJECT_FILESYSTEM_DEFAULTS;
369375

376+
/* Avoid allocating a new string if we can avoid it */
377+
if (index(name, '\\')) {
378+
if ((error = git_path_normalize_slashes(&buf, name)) < 0)
379+
return error;
380+
} else {
381+
git_buf_attach_notowned(&buf, name, strlen(name));
382+
}
383+
370384
/* FIXME: Un-consting it to reduce the amount of diff */
371-
return git_path_isvalid((git_repository *)repo, name, flags);
385+
isvalid = git_path_isvalid((git_repository *)repo, buf.ptr, flags);
386+
git_buf_free(&buf);
387+
388+
return isvalid;
372389
}
373390

374391
static void submodule_free_dup(void *sm)
@@ -1571,16 +1588,17 @@ static int submodule_update_head(git_submodule *submodule)
15711588

15721589
int git_submodule_reload(git_submodule *sm, int force)
15731590
{
1574-
int error = 0;
1591+
int error = 0, isvalid;
15751592
git_config *mods;
15761593

15771594
GIT_UNUSED(force);
15781595

15791596
assert(sm);
15801597

1581-
if (!git_submodule_name_is_valid(sm->repo, sm->name, 0)) {
1598+
isvalid = git_submodule_name_is_valid(sm->repo, sm->name, 0);
1599+
if (isvalid <= 0) {
15821600
/* This should come with a warning, but we've no API for that */
1583-
return 0;
1601+
return isvalid;
15841602
}
15851603

15861604
if (!git_repository_is_bare(sm->repo)) {
@@ -1924,7 +1942,7 @@ static int submodule_load_each(const git_config_entry *entry, void *payload)
19241942
git_strmap *map = data->map;
19251943
git_buf name = GIT_BUF_INIT;
19261944
git_submodule *sm;
1927-
int error;
1945+
int error, isvalid;
19281946

19291947
if (git__prefixcmp(entry->name, "submodule.") != 0)
19301948
return 0;
@@ -1940,8 +1958,9 @@ static int submodule_load_each(const git_config_entry *entry, void *payload)
19401958
if ((error = git_buf_set(&name, namestart, property - namestart -1)) < 0)
19411959
return error;
19421960

1943-
if (!git_path_isvalid(data->repo, name.ptr, GIT_PATH_REJECT_TRAVERSAL)) {
1944-
error = 0;
1961+
isvalid = git_submodule_name_is_valid(data->repo, name.ptr, 0);
1962+
if (isvalid <= 0) {
1963+
error = isvalid;
19451964
goto done;
19461965
}
19471966

tests/submodule/escape.c

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@ void test_submodule_escape__cleanup(void)
1313
}
1414

1515
#define EVIL_SM_NAME "../../modules/evil"
16+
#define EVIL_SM_NAME_WINDOWS "..\\\\..\\\\modules\\\\evil"
17+
#define EVIL_SM_NAME_WINDOWS_UNESC "..\\..\\modules\\evil"
1618

1719
static int find_evil(git_submodule *sm, const char *name, void *payload)
1820
{
1921
int *foundit = (int *) payload;
2022

2123
GIT_UNUSED(sm);
2224

23-
if (!git__strcmp(EVIL_SM_NAME, name))
25+
if (!git__strcmp(EVIL_SM_NAME, name) ||
26+
!git__strcmp(EVIL_SM_NAME_WINDOWS_UNESC, name))
2427
*foundit = true;
2528

2629
return 0;
@@ -29,7 +32,6 @@ static int find_evil(git_submodule *sm, const char *name, void *payload)
2932
void test_submodule_escape__from_gitdir(void)
3033
{
3134
int foundit;
32-
git_config *cfg;
3335
git_submodule *sm;
3436
git_buf buf = GIT_BUF_INIT;
3537
unsigned int sm_location;
@@ -42,10 +44,6 @@ void test_submodule_escape__from_gitdir(void)
4244
" path = testrepo\n"
4345
" url = ../testrepo.git\n");
4446

45-
/* We also need to update the value in the config */
46-
cl_git_pass(git_repository_config__weakptr(&cfg, g_repo));
47-
cfg = NULL;
48-
4947
/* Find it all the different ways we know about it */
5048
foundit = 0;
5149
cl_git_pass(git_submodule_foreach(g_repo, find_evil, &foundit));
@@ -63,3 +61,36 @@ void test_submodule_escape__from_gitdir(void)
6361
cl_assert_equal_i(GIT_SUBMODULE_STATUS_IN_INDEX | GIT_SUBMODULE_STATUS_IN_HEAD, sm_location);
6462
git_submodule_free(sm);
6563
}
64+
65+
void test_submodule_escape__from_gitdir_windows(void)
66+
{
67+
int foundit;
68+
git_submodule *sm;
69+
git_buf buf = GIT_BUF_INIT;
70+
unsigned int sm_location;
71+
72+
g_repo = setup_fixture_submodule_simple();
73+
74+
cl_git_pass(git_buf_joinpath(&buf, git_repository_workdir(g_repo), ".gitmodules"));
75+
cl_git_rewritefile(buf.ptr,
76+
"[submodule \"" EVIL_SM_NAME_WINDOWS "\"]\n"
77+
" path = testrepo\n"
78+
" url = ../testrepo.git\n");
79+
80+
/* Find it all the different ways we know about it */
81+
foundit = 0;
82+
cl_git_pass(git_submodule_foreach(g_repo, find_evil, &foundit));
83+
cl_assert_equal_i(0, foundit);
84+
cl_git_fail_with(GIT_ENOTFOUND, git_submodule_lookup(&sm, g_repo, EVIL_SM_NAME_WINDOWS_UNESC));
85+
/*
86+
* We do know about this as it's in the index and HEAD, but the data is
87+
* incomplete as there is no configured data for it (we pretend it
88+
* doesn't exist). This leaves us with an odd situation but it's
89+
* consistent with what we would do if we did add a submodule with no
90+
* configuration.
91+
*/
92+
cl_git_pass(git_submodule_lookup(&sm, g_repo, "testrepo"));
93+
cl_git_pass(git_submodule_location(&sm_location, sm));
94+
cl_assert_equal_i(GIT_SUBMODULE_STATUS_IN_INDEX | GIT_SUBMODULE_STATUS_IN_HEAD, sm_location);
95+
git_submodule_free(sm);
96+
}

0 commit comments

Comments
 (0)