Skip to content

Commit 5532ebd

Browse files
committed
Merge branch 'fix-mingw-quoting-bug'
This patch fixes a vulnerability in the Windows-specific code where a submodule names ending in a backslash were quoted incorrectly, and that bug could be abused to insert command-line parameters e.g. to `ssh` in a recursive clone. Note: this bug is Windows-only, as we have to construct a command line for the process-to-spawn, unlike Linux/macOS, where `execv()` accepts an already-split command line. While at it, other quoting issues are fixed as well. Signed-off-by: Johannes Schindelin <[email protected]>
2 parents 76a681c + 379e51d commit 5532ebd

File tree

3 files changed

+156
-5
lines changed

3 files changed

+156
-5
lines changed

compat/mingw.c

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -872,7 +872,7 @@ static const char *quote_arg(const char *arg)
872872
p++;
873873
len++;
874874
}
875-
if (*p == '"')
875+
if (*p == '"' || !*p)
876876
n += count*2 + 1;
877877
continue;
878878
}
@@ -894,16 +894,19 @@ static const char *quote_arg(const char *arg)
894894
count++;
895895
*d++ = *arg++;
896896
}
897-
if (*arg == '"') {
897+
if (*arg == '"' || !*arg) {
898898
while (count-- > 0)
899899
*d++ = '\\';
900+
/* don't escape the surrounding end quote */
901+
if (!*arg)
902+
break;
900903
*d++ = '\\';
901904
}
902905
}
903906
*d++ = *arg++;
904907
}
905908
*d++ = '"';
906-
*d++ = 0;
909+
*d++ = '\0';
907910
return q;
908911
}
909912

t/helper/test-run-command.c

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
#include "run-command.h"
1313
#include "argv-array.h"
1414
#include "strbuf.h"
15-
#include <string.h>
16-
#include <errno.h>
15+
#include "gettext.h"
16+
#include "parse-options.h"
1717

1818
static int number_callbacks;
1919
static int parallel_next(struct child_process *cp,
@@ -49,11 +49,145 @@ static int task_finished(int result,
4949
return 1;
5050
}
5151

52+
static uint64_t my_random_next = 1234;
53+
54+
static uint64_t my_random(void)
55+
{
56+
uint64_t res = my_random_next;
57+
my_random_next = my_random_next * 1103515245 + 12345;
58+
return res;
59+
}
60+
61+
static int quote_stress_test(int argc, const char **argv)
62+
{
63+
/*
64+
* We are running a quote-stress test.
65+
* spawn a subprocess that runs quote-stress with a
66+
* special option that echoes back the arguments that
67+
* were passed in.
68+
*/
69+
char special[] = ".?*\\^_\"'`{}()[]<>@~&+:;$%"; // \t\r\n\a";
70+
int i, j, k, trials = 100, skip = 0, msys2 = 0;
71+
struct strbuf out = STRBUF_INIT;
72+
struct argv_array args = ARGV_ARRAY_INIT;
73+
struct option options[] = {
74+
OPT_INTEGER('n', "trials", &trials, "Number of trials"),
75+
OPT_INTEGER('s', "skip", &skip, "Skip <n> trials"),
76+
OPT_BOOL('m', "msys2", &msys2, "Test quoting for MSYS2's sh"),
77+
OPT_END()
78+
};
79+
const char * const usage[] = {
80+
"test-run-command quote-stress-test <options>",
81+
NULL
82+
};
83+
84+
argc = parse_options(argc, argv, NULL, options, usage, 0);
85+
86+
setenv("MSYS_NO_PATHCONV", "1", 0);
87+
88+
for (i = 0; i < trials; i++) {
89+
struct child_process cp = CHILD_PROCESS_INIT;
90+
size_t arg_count, arg_offset;
91+
int ret = 0;
92+
93+
argv_array_clear(&args);
94+
if (msys2)
95+
argv_array_pushl(&args, "sh", "-c",
96+
"printf %s\\\\0 \"$@\"", "skip", NULL);
97+
else
98+
argv_array_pushl(&args, "test-run-command",
99+
"quote-echo", NULL);
100+
arg_offset = args.argc;
101+
102+
if (argc > 0) {
103+
trials = 1;
104+
arg_count = argc;
105+
for (j = 0; j < arg_count; j++)
106+
argv_array_push(&args, argv[j]);
107+
} else {
108+
arg_count = 1 + (my_random() % 5);
109+
for (j = 0; j < arg_count; j++) {
110+
char buf[20];
111+
size_t min_len = 1;
112+
size_t arg_len = min_len +
113+
(my_random() % (ARRAY_SIZE(buf) - min_len));
114+
115+
for (k = 0; k < arg_len; k++)
116+
buf[k] = special[my_random() %
117+
ARRAY_SIZE(special)];
118+
buf[arg_len] = '\0';
119+
120+
argv_array_push(&args, buf);
121+
}
122+
}
123+
124+
if (i < skip)
125+
continue;
126+
127+
cp.argv = args.argv;
128+
strbuf_reset(&out);
129+
if (pipe_command(&cp, NULL, 0, &out, 0, NULL, 0) < 0)
130+
return error("Failed to spawn child process");
131+
132+
for (j = 0, k = 0; j < arg_count; j++) {
133+
const char *arg = args.argv[j + arg_offset];
134+
135+
if (strcmp(arg, out.buf + k))
136+
ret = error("incorrectly quoted arg: '%s', "
137+
"echoed back as '%s'",
138+
arg, out.buf + k);
139+
k += strlen(out.buf + k) + 1;
140+
}
141+
142+
if (k != out.len)
143+
ret = error("got %d bytes, but consumed only %d",
144+
(int)out.len, (int)k);
145+
146+
if (ret) {
147+
fprintf(stderr, "Trial #%d failed. Arguments:\n", i);
148+
for (j = 0; j < arg_count; j++)
149+
fprintf(stderr, "arg #%d: '%s'\n",
150+
(int)j, args.argv[j + arg_offset]);
151+
152+
strbuf_release(&out);
153+
argv_array_clear(&args);
154+
155+
return ret;
156+
}
157+
158+
if (i && (i % 100) == 0)
159+
fprintf(stderr, "Trials completed: %d\n", (int)i);
160+
}
161+
162+
strbuf_release(&out);
163+
argv_array_clear(&args);
164+
165+
return 0;
166+
}
167+
168+
static int quote_echo(int argc, const char **argv)
169+
{
170+
while (argc > 1) {
171+
fwrite(argv[1], strlen(argv[1]), 1, stdout);
172+
fputc('\0', stdout);
173+
argv++;
174+
argc--;
175+
}
176+
177+
return 0;
178+
}
179+
52180
int cmd_main(int argc, const char **argv)
53181
{
54182
struct child_process proc = CHILD_PROCESS_INIT;
55183
int jobs;
56184

185+
if (argc >= 2 && !strcmp(argv[1], "quote-stress-test"))
186+
return !!quote_stress_test(argc - 1, argv + 1);
187+
188+
if (argc >= 2 && !strcmp(argv[1], "quote-echo"))
189+
return !!quote_echo(argc - 1, argv + 1);
190+
57191
if (argc < 3)
58192
return 1;
59193
proc.argv = (const char **)argv + 2;

t/t7416-submodule-dash-url.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,18 @@ test_expect_success 'clone rejects unprotected dash' '
3131
test_i18ngrep ignoring err
3232
'
3333

34+
test_expect_success 'trailing backslash is handled correctly' '
35+
git init testmodule &&
36+
test_commit -C testmodule c &&
37+
git submodule add ./testmodule &&
38+
: ensure that the name ends in a double backslash &&
39+
sed -e "s|\\(submodule \"testmodule\\)\"|\\1\\\\\\\\\"|" \
40+
-e "s|url = .*|url = \" --should-not-be-an-option\"|" \
41+
<.gitmodules >.new &&
42+
mv .new .gitmodules &&
43+
git commit -am "Add testmodule" &&
44+
test_must_fail git clone --verbose --recurse-submodules . dolly 2>err &&
45+
test_i18ngrep ! "unknown option" err
46+
'
47+
3448
test_done

0 commit comments

Comments
 (0)