Skip to content

Commit 1c07901

Browse files
committed
Merge tag 'v2.47.1.windows.2' into rebase-to-v2.48.0
This merges Git for Windows v2.47.1(2) which was released on January 14th, 2025, in an embargoed release that was massively coordinated between GitHub Desktop, Visual Studio, Git Credential Manager, Git LFS, Git and Git for Windows. Most notably, this merges in the fixes for: * CVE-2024-50349: When prompting the user for a password in the terminal, Git does not neutralize control characters. * CVE-2024-52005: The sideband channel does not neutralize control characters. * CVE-2024-52006: Similar to CVE-2020-5260, affecting credential helpers that interpret Carriage Returns as newlines. Signed-off-by: Johannes Schindelin <[email protected]>
2 parents 81e36ea + 36a139e commit 1c07901

14 files changed

+243
-41
lines changed

Documentation/config.txt

+2
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,8 @@ include::config/sequencer.txt[]
524524

525525
include::config/showbranch.txt[]
526526

527+
include::config/sideband.txt[]
528+
527529
include::config/sparse.txt[]
528530

529531
include::config/splitindex.txt[]

Documentation/config/credential.txt

+11
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ credential.useHttpPath::
2222
or https URL to be important. Defaults to false. See
2323
linkgit:gitcredentials[7] for more information.
2424

25+
credential.sanitizePrompt::
26+
By default, user names and hosts that are shown as part of the
27+
password prompt are not allowed to contain control characters (they
28+
will be URL-encoded by default). Configure this setting to `false` to
29+
override that behavior.
30+
31+
credential.protectProtocol::
32+
By default, Carriage Return characters are not allowed in the protocol
33+
that is used when Git talks to a credential helper. This setting allows
34+
users to override this default.
35+
2536
credential.username::
2637
If no username is set for a network authentication, use this username
2738
by default. See credential.<context>.* below, and

Documentation/config/sideband.txt

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
sideband.allowControlCharacters::
2+
By default, control characters that are delivered via the sideband
3+
are masked, except ANSI color sequences. This prevents potentially
4+
unwanted ANSI escape sequences from being sent to the terminal. Use
5+
this config setting to override this behavior:
6+
+
7+
--
8+
color::
9+
Allow ANSI color sequences, line feeds and horizontal tabs,
10+
but mask all other control characters. This is the default.
11+
false::
12+
Mask all control characters other than line feeds and
13+
horizontal tabs.
14+
true::
15+
Allow all control characters to be sent to the terminal.
16+
--

credential.c

+31-18
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ static int credential_config_callback(const char *var, const char *value,
130130
}
131131
else if (!strcmp(key, "usehttppath"))
132132
c->use_http_path = git_config_bool(var, value);
133+
else if (!strcmp(key, "sanitizeprompt"))
134+
c->sanitize_prompt = git_config_bool(var, value);
135+
else if (!strcmp(key, "protectprotocol"))
136+
c->protect_protocol = git_config_bool(var, value);
133137

134138
return 0;
135139
}
@@ -227,7 +231,8 @@ static void credential_format(struct credential *c, struct strbuf *out)
227231
strbuf_addch(out, '@');
228232
}
229233
if (c->host)
230-
strbuf_addstr(out, c->host);
234+
strbuf_add_percentencode(out, c->host,
235+
STRBUF_ENCODE_HOST_AND_PORT);
231236
if (c->path) {
232237
strbuf_addch(out, '/');
233238
strbuf_add_percentencode(out, c->path, 0);
@@ -241,7 +246,10 @@ static char *credential_ask_one(const char *what, struct credential *c,
241246
struct strbuf prompt = STRBUF_INIT;
242247
char *r;
243248

244-
credential_describe(c, &desc);
249+
if (c->sanitize_prompt)
250+
credential_format(c, &desc);
251+
else
252+
credential_describe(c, &desc);
245253
if (desc.len)
246254
strbuf_addf(&prompt, "%s for '%s': ", what, desc.buf);
247255
else
@@ -382,7 +390,8 @@ int credential_read(struct credential *c, FILE *fp,
382390
return 0;
383391
}
384392

385-
static void credential_write_item(FILE *fp, const char *key, const char *value,
393+
static void credential_write_item(const struct credential *c,
394+
FILE *fp, const char *key, const char *value,
386395
int required)
387396
{
388397
if (!value && required)
@@ -391,41 +400,45 @@ static void credential_write_item(FILE *fp, const char *key, const char *value,
391400
return;
392401
if (strchr(value, '\n'))
393402
die("credential value for %s contains newline", key);
403+
if (c->protect_protocol && strchr(value, '\r'))
404+
die("credential value for %s contains carriage return\n"
405+
"If this is intended, set `credential.protectProtocol=false`",
406+
key);
394407
fprintf(fp, "%s=%s\n", key, value);
395408
}
396409

397410
void credential_write(const struct credential *c, FILE *fp,
398411
enum credential_op_type op_type)
399412
{
400413
if (credential_has_capability(&c->capa_authtype, op_type))
401-
credential_write_item(fp, "capability[]", "authtype", 0);
414+
credential_write_item(c, fp, "capability[]", "authtype", 0);
402415
if (credential_has_capability(&c->capa_state, op_type))
403-
credential_write_item(fp, "capability[]", "state", 0);
416+
credential_write_item(c, fp, "capability[]", "state", 0);
404417

405418
if (credential_has_capability(&c->capa_authtype, op_type)) {
406-
credential_write_item(fp, "authtype", c->authtype, 0);
407-
credential_write_item(fp, "credential", c->credential, 0);
419+
credential_write_item(c, fp, "authtype", c->authtype, 0);
420+
credential_write_item(c, fp, "credential", c->credential, 0);
408421
if (c->ephemeral)
409-
credential_write_item(fp, "ephemeral", "1", 0);
422+
credential_write_item(c, fp, "ephemeral", "1", 0);
410423
}
411-
credential_write_item(fp, "protocol", c->protocol, 1);
412-
credential_write_item(fp, "host", c->host, 1);
413-
credential_write_item(fp, "path", c->path, 0);
414-
credential_write_item(fp, "username", c->username, 0);
415-
credential_write_item(fp, "password", c->password, 0);
416-
credential_write_item(fp, "oauth_refresh_token", c->oauth_refresh_token, 0);
424+
credential_write_item(c, fp, "protocol", c->protocol, 1);
425+
credential_write_item(c, fp, "host", c->host, 1);
426+
credential_write_item(c, fp, "path", c->path, 0);
427+
credential_write_item(c, fp, "username", c->username, 0);
428+
credential_write_item(c, fp, "password", c->password, 0);
429+
credential_write_item(c, fp, "oauth_refresh_token", c->oauth_refresh_token, 0);
417430
if (c->password_expiry_utc != TIME_MAX) {
418431
char *s = xstrfmt("%"PRItime, c->password_expiry_utc);
419-
credential_write_item(fp, "password_expiry_utc", s, 0);
432+
credential_write_item(c, fp, "password_expiry_utc", s, 0);
420433
free(s);
421434
}
422435
for (size_t i = 0; i < c->wwwauth_headers.nr; i++)
423-
credential_write_item(fp, "wwwauth[]", c->wwwauth_headers.v[i], 0);
436+
credential_write_item(c, fp, "wwwauth[]", c->wwwauth_headers.v[i], 0);
424437
if (credential_has_capability(&c->capa_state, op_type)) {
425438
if (c->multistage)
426-
credential_write_item(fp, "continue", "1", 0);
439+
credential_write_item(c, fp, "continue", "1", 0);
427440
for (size_t i = 0; i < c->state_headers_to_send.nr; i++)
428-
credential_write_item(fp, "state[]", c->state_headers_to_send.v[i], 0);
441+
credential_write_item(c, fp, "state[]", c->state_headers_to_send.v[i], 0);
429442
}
430443
}
431444

credential.h

+5-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,9 @@ struct credential {
168168
multistage: 1,
169169
quit:1,
170170
use_http_path:1,
171-
username_from_proto:1;
171+
username_from_proto:1,
172+
sanitize_prompt:1,
173+
protect_protocol:1;
172174

173175
struct credential_capability capa_authtype;
174176
struct credential_capability capa_state;
@@ -195,6 +197,8 @@ struct credential {
195197
.wwwauth_headers = STRVEC_INIT, \
196198
.state_headers = STRVEC_INIT, \
197199
.state_headers_to_send = STRVEC_INIT, \
200+
.sanitize_prompt = 1, \
201+
.protect_protocol = 1, \
198202
}
199203

200204
/* Initialize a credential structure, setting all fields to empty. */

sideband.c

+76-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ static struct keyword_entry keywords[] = {
2626
{ "error", GIT_COLOR_BOLD_RED },
2727
};
2828

29+
static enum {
30+
ALLOW_NO_CONTROL_CHARACTERS = 0,
31+
ALLOW_ALL_CONTROL_CHARACTERS = 1,
32+
ALLOW_ANSI_COLOR_SEQUENCES = 2
33+
} allow_control_characters = ALLOW_ANSI_COLOR_SEQUENCES;
34+
2935
/* Returns a color setting (GIT_COLOR_NEVER, etc). */
3036
static int use_sideband_colors(void)
3137
{
@@ -39,6 +45,25 @@ static int use_sideband_colors(void)
3945
if (use_sideband_colors_cached >= 0)
4046
return use_sideband_colors_cached;
4147

48+
switch (git_config_get_maybe_bool("sideband.allowcontrolcharacters", &i)) {
49+
case 0: /* Boolean value */
50+
allow_control_characters = i ? ALLOW_ALL_CONTROL_CHARACTERS :
51+
ALLOW_NO_CONTROL_CHARACTERS;
52+
break;
53+
case -1: /* non-Boolean value */
54+
if (git_config_get_string_tmp("sideband.allowcontrolcharacters",
55+
&value))
56+
; /* huh? `get_maybe_bool()` returned -1 */
57+
else if (!strcmp(value, "color"))
58+
allow_control_characters = ALLOW_ANSI_COLOR_SEQUENCES;
59+
else
60+
warning(_("unrecognized value for `sideband."
61+
"allowControlCharacters`: '%s'"), value);
62+
break;
63+
default:
64+
break; /* not configured */
65+
}
66+
4267
if (!git_config_get_string_tmp(key, &value))
4368
use_sideband_colors_cached = git_config_colorbool(key, value);
4469
else if (!git_config_get_string_tmp("color.ui", &value))
@@ -66,6 +91,55 @@ void list_config_color_sideband_slots(struct string_list *list, const char *pref
6691
list_config_item(list, prefix, keywords[i].keyword);
6792
}
6893

94+
static int handle_ansi_color_sequence(struct strbuf *dest, const char *src, int n)
95+
{
96+
int i;
97+
98+
/*
99+
* Valid ANSI color sequences are of the form
100+
*
101+
* ESC [ [<n> [; <n>]*] m
102+
*/
103+
104+
if (allow_control_characters != ALLOW_ANSI_COLOR_SEQUENCES ||
105+
n < 3 || src[0] != '\x1b' || src[1] != '[')
106+
return 0;
107+
108+
for (i = 2; i < n; i++) {
109+
if (src[i] == 'm') {
110+
strbuf_add(dest, src, i + 1);
111+
return i;
112+
}
113+
if (!isdigit(src[i]) && src[i] != ';')
114+
break;
115+
}
116+
117+
return 0;
118+
}
119+
120+
static void strbuf_add_sanitized(struct strbuf *dest, const char *src, int n)
121+
{
122+
int i;
123+
124+
if (allow_control_characters == ALLOW_ALL_CONTROL_CHARACTERS) {
125+
strbuf_add(dest, src, n);
126+
return;
127+
}
128+
129+
strbuf_grow(dest, n);
130+
for (; n && *src; src++, n--) {
131+
if (!iscntrl(*src) || *src == '\t' || *src == '\n')
132+
strbuf_addch(dest, *src);
133+
else if ((i = handle_ansi_color_sequence(dest, src, n))) {
134+
src += i;
135+
n -= i;
136+
} else {
137+
strbuf_addch(dest, '^');
138+
strbuf_addch(dest, 0x40 + *src);
139+
}
140+
}
141+
}
142+
69143
/*
70144
* Optionally highlight one keyword in remote output if it appears at the start
71145
* of the line. This should be called for a single line only, which is
@@ -81,7 +155,7 @@ static void maybe_colorize_sideband(struct strbuf *dest, const char *src, int n)
81155
int i;
82156

83157
if (!want_color_stderr(use_sideband_colors())) {
84-
strbuf_add(dest, src, n);
158+
strbuf_add_sanitized(dest, src, n);
85159
return;
86160
}
87161

@@ -114,7 +188,7 @@ static void maybe_colorize_sideband(struct strbuf *dest, const char *src, int n)
114188
}
115189
}
116190

117-
strbuf_add(dest, src, n);
191+
strbuf_add_sanitized(dest, src, n);
118192
}
119193

120194

strbuf.c

+3-1
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,9 @@ void strbuf_add_percentencode(struct strbuf *dst, const char *src, int flags)
497497
unsigned char ch = src[i];
498498
if (ch <= 0x1F || ch >= 0x7F ||
499499
(ch == '/' && (flags & STRBUF_ENCODE_SLASH)) ||
500-
strchr(URL_UNSAFE_CHARS, ch))
500+
((flags & STRBUF_ENCODE_HOST_AND_PORT) ?
501+
!isalnum(ch) && !strchr("-.:[]", ch) :
502+
!!strchr(URL_UNSAFE_CHARS, ch)))
501503
strbuf_addf(dst, "%%%02X", (unsigned char)ch);
502504
else
503505
strbuf_addch(dst, ch);

strbuf.h

+1
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ void strbuf_expand_bad_format(const char *format, const char *command);
356356
void strbuf_addbuf_percentquote(struct strbuf *dst, const struct strbuf *src);
357357

358358
#define STRBUF_ENCODE_SLASH 1
359+
#define STRBUF_ENCODE_HOST_AND_PORT 2
359360

360361
/**
361362
* Append the contents of a string to a strbuf, percent-encoding any characters

t/t0300-credentials.sh

+49
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ test_expect_success 'setup helper scripts' '
7676
test -z "$pexpiry" || echo password_expiry_utc=$pexpiry
7777
EOF
7878
79+
write_script git-credential-cntrl-in-username <<-\EOF &&
80+
printf "username=\\007latrix Lestrange\\n"
81+
EOF
82+
7983
PATH="$PWD$PATH_SEP$PATH"
8084
'
8185

@@ -696,6 +700,19 @@ test_expect_success 'match percent-encoded values in username' '
696700
EOF
697701
'
698702

703+
test_expect_success 'match percent-encoded values in hostname' '
704+
test_config "credential.https://a%20b%20c/.helper" "$HELPER" &&
705+
check fill <<-\EOF
706+
url=https://a b c/
707+
--
708+
protocol=https
709+
host=a b c
710+
username=foo
711+
password=bar
712+
--
713+
EOF
714+
'
715+
699716
test_expect_success 'fetch with multiple path components' '
700717
test_unconfig credential.helper &&
701718
test_config credential.https://example.com/foo/repo.git.helper "verbatim foo bar" &&
@@ -885,6 +902,22 @@ test_expect_success 'url parser rejects embedded newlines' '
885902
test_cmp expect stderr
886903
'
887904

905+
test_expect_success 'url parser rejects embedded carriage returns' '
906+
test_config credential.helper "!true" &&
907+
test_must_fail git credential fill 2>stderr <<-\EOF &&
908+
url=https://example%0d.com/
909+
EOF
910+
cat >expect <<-\EOF &&
911+
fatal: credential value for host contains carriage return
912+
If this is intended, set `credential.protectProtocol=false`
913+
EOF
914+
test_cmp expect stderr &&
915+
GIT_ASKPASS=true \
916+
git -c credential.protectProtocol=false credential fill <<-\EOF
917+
url=https://example%0d.com/
918+
EOF
919+
'
920+
888921
test_expect_success 'host-less URLs are parsed as empty host' '
889922
check fill "verbatim foo bar" <<-\EOF
890923
url=cert:///path/to/cert.pem
@@ -994,4 +1027,20 @@ test_expect_success 'credential config with partial URLs' '
9941027
test_grep "skipping credential lookup for key" stderr
9951028
'
9961029

1030+
BEL="$(printf '\007')"
1031+
1032+
test_expect_success 'interactive prompt is sanitized' '
1033+
check fill cntrl-in-username <<-EOF
1034+
protocol=https
1035+
host=example.org
1036+
--
1037+
protocol=https
1038+
host=example.org
1039+
username=${BEL}latrix Lestrange
1040+
password=askpass-password
1041+
--
1042+
askpass: Password for ${SQ}https://%07latrix%[email protected]${SQ}:
1043+
EOF
1044+
'
1045+
9971046
test_done

0 commit comments

Comments
 (0)