Skip to content

Commit f04024b

Browse files
committed
Merge branch 'disallow-control-characters-in-credential-urls-by-default'
This addresses two vulnerabilities: - CVE-2024-50349: Printing unsanitized URLs when asking for credentials made the user susceptible to crafted URLs (e.g. in recursive clones) that mislead the user into typing in passwords for trusted sites that would then be sent to untrusted sites instead. - CVE-2024-52006 Git may pass on Carriage Returns via the credential protocol to credential helpers which use line-reading functions that interpret said Carriage Returns as line endings, even though Git did not intend that. Signed-off-by: Johannes Schindelin <[email protected]>
2 parents ee1479b + 429023c commit f04024b

File tree

9 files changed

+118
-38
lines changed

9 files changed

+118
-38
lines changed

Documentation/config/credential.txt

Lines changed: 11 additions & 0 deletions
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

credential.c

Lines changed: 31 additions & 18 deletions
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

Lines changed: 5 additions & 1 deletion
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. */

strbuf.c

Lines changed: 3 additions & 1 deletion
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 49 additions & 0 deletions
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"
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

t/t5541-http-push-smart.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ test_expect_success 'push over smart http with auth' '
343343
git push "$HTTPD_URL"/auth/smart/test_repo.git &&
344344
git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/test_repo.git" \
345345
log -1 --format=%s >actual &&
346-
expect_askpass both user@host &&
346+
expect_askpass both user%40host &&
347347
test_cmp expect actual
348348
'
349349

@@ -355,7 +355,7 @@ test_expect_success 'push to auth-only-for-push repo' '
355355
git push "$HTTPD_URL"/auth-push/smart/test_repo.git &&
356356
git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/test_repo.git" \
357357
log -1 --format=%s >actual &&
358-
expect_askpass both user@host &&
358+
expect_askpass both user%40host &&
359359
test_cmp expect actual
360360
'
361361

@@ -385,7 +385,7 @@ test_expect_success 'push into half-auth-complete requires password' '
385385
git push "$HTTPD_URL/half-auth-complete/smart/half-auth.git" &&
386386
git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/half-auth.git" \
387387
log -1 --format=%s >actual &&
388-
expect_askpass both user@host &&
388+
expect_askpass both user%40host &&
389389
test_cmp expect actual
390390
'
391391

t/t5550-http-fetch-dumb.sh

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,13 @@ test_expect_success 'http auth can use user/pass in URL' '
111111
test_expect_success 'http auth can use just user in URL' '
112112
set_askpass wrong pass@host &&
113113
git clone "$HTTPD_URL_USER/auth/dumb/repo.git" clone-auth-pass &&
114-
expect_askpass pass user@host
114+
expect_askpass pass user%40host
115115
'
116116

117117
test_expect_success 'http auth can request both user and pass' '
118118
set_askpass user@host pass@host &&
119119
git clone "$HTTPD_URL/auth/dumb/repo.git" clone-auth-both &&
120-
expect_askpass both user@host
120+
expect_askpass both user%40host
121121
'
122122

123123
test_expect_success 'http auth respects credential helper config' '
@@ -135,14 +135,14 @@ test_expect_success 'http auth can get username from config' '
135135
test_config_global "credential.$HTTPD_URL.username" user@host &&
136136
set_askpass wrong pass@host &&
137137
git clone "$HTTPD_URL/auth/dumb/repo.git" clone-auth-user &&
138-
expect_askpass pass user@host
138+
expect_askpass pass user%40host
139139
'
140140

141141
test_expect_success 'configured username does not override URL' '
142142
test_config_global "credential.$HTTPD_URL.username" wrong &&
143143
set_askpass wrong pass@host &&
144144
git clone "$HTTPD_URL_USER/auth/dumb/repo.git" clone-auth-user2 &&
145-
expect_askpass pass user@host
145+
expect_askpass pass user%40host
146146
'
147147

148148
test_expect_success 'set up repo with http submodules' '
@@ -163,7 +163,7 @@ test_expect_success 'cmdline credential config passes to submodule via clone' '
163163
set_askpass wrong pass@host &&
164164
git -c "credential.$HTTPD_URL.username=user@host" \
165165
clone --recursive super super-clone &&
166-
expect_askpass pass user@host
166+
expect_askpass pass user%40host
167167
'
168168

169169
test_expect_success 'cmdline credential config passes submodule via fetch' '
@@ -174,7 +174,7 @@ test_expect_success 'cmdline credential config passes submodule via fetch' '
174174
git -C super-clone \
175175
-c "credential.$HTTPD_URL.username=user@host" \
176176
fetch --recurse-submodules &&
177-
expect_askpass pass user@host
177+
expect_askpass pass user%40host
178178
'
179179

180180
test_expect_success 'cmdline credential config passes submodule update' '
@@ -191,7 +191,7 @@ test_expect_success 'cmdline credential config passes submodule update' '
191191
git -C super-clone \
192192
-c "credential.$HTTPD_URL.username=user@host" \
193193
submodule update &&
194-
expect_askpass pass user@host
194+
expect_askpass pass user%40host
195195
'
196196

197197
test_expect_success 'fetch changes via http' '

t/t5551-http-fetch-smart.sh

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ test_expect_success 'clone from password-protected repository' '
181181
echo two >expect &&
182182
set_askpass user@host pass@host &&
183183
git clone --bare "$HTTPD_URL/auth/smart/repo.git" smart-auth &&
184-
expect_askpass both user@host &&
184+
expect_askpass both user%40host &&
185185
git --git-dir=smart-auth log -1 --format=%s >actual &&
186186
test_cmp expect actual
187187
'
@@ -221,7 +221,7 @@ test_expect_success 'clone from auth-only-for-objects repository' '
221221
echo two >expect &&
222222
set_askpass user@host pass@host &&
223223
git clone --bare "$HTTPD_URL/auth-fetch/smart/repo.git" half-auth &&
224-
expect_askpass both user@host &&
224+
expect_askpass both user%40host &&
225225
git --git-dir=half-auth log -1 --format=%s >actual &&
226226
test_cmp expect actual
227227
'
@@ -246,14 +246,14 @@ test_expect_success 'redirects send auth to new location' '
246246
set_askpass user@host pass@host &&
247247
git -c credential.useHttpPath=true \
248248
clone $HTTPD_URL/smart-redir-auth/repo.git repo-redir-auth &&
249-
expect_askpass both user@host auth/smart/repo.git
249+
expect_askpass both user%40host auth/smart/repo.git
250250
'
251251

252252
test_expect_success 'GIT_TRACE_CURL redacts auth details' '
253253
rm -rf redact-auth trace &&
254254
set_askpass user@host pass@host &&
255255
GIT_TRACE_CURL="$(pwd)/trace" git clone --bare "$HTTPD_URL/auth/smart/repo.git" redact-auth &&
256-
expect_askpass both user@host &&
256+
expect_askpass both user%40host &&
257257
258258
# Ensure that there is no "Basic" followed by a base64 string, but that
259259
# the auth details are redacted
@@ -265,7 +265,7 @@ test_expect_success 'GIT_CURL_VERBOSE redacts auth details' '
265265
rm -rf redact-auth trace &&
266266
set_askpass user@host pass@host &&
267267
GIT_CURL_VERBOSE=1 git clone --bare "$HTTPD_URL/auth/smart/repo.git" redact-auth 2>trace &&
268-
expect_askpass both user@host &&
268+
expect_askpass both user%40host &&
269269
270270
# Ensure that there is no "Basic" followed by a base64 string, but that
271271
# the auth details are redacted
@@ -278,7 +278,7 @@ test_expect_success 'GIT_TRACE_CURL does not redact auth details if GIT_TRACE_RE
278278
set_askpass user@host pass@host &&
279279
GIT_TRACE_REDACT=0 GIT_TRACE_CURL="$(pwd)/trace" \
280280
git clone --bare "$HTTPD_URL/auth/smart/repo.git" redact-auth &&
281-
expect_askpass both user@host &&
281+
expect_askpass both user%40host &&
282282
283283
grep -i "Authorization: Basic [0-9a-zA-Z+/]" trace
284284
'
@@ -592,7 +592,7 @@ test_expect_success 'http auth remembers successful credentials' '
592592
# the first request prompts the user...
593593
set_askpass user@host pass@host &&
594594
git ls-remote "$HTTPD_URL/auth/smart/repo.git" >/dev/null &&
595-
expect_askpass both user@host &&
595+
expect_askpass both user%40host &&
596596
597597
# ...and the second one uses the stored value rather than
598598
# prompting the user.
@@ -623,7 +623,7 @@ test_expect_success 'http auth forgets bogus credentials' '
623623
# us to prompt the user again.
624624
set_askpass user@host pass@host &&
625625
git ls-remote "$HTTPD_URL/auth/smart/repo.git" >/dev/null &&
626-
expect_askpass both user@host
626+
expect_askpass both user%40host
627627
'
628628

629629
test_expect_success 'client falls back from v2 to v0 to match server' '

0 commit comments

Comments
 (0)