Skip to content

Commit 0875613

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 062d9fb + b01b9b8 commit 0875613

File tree

9 files changed

+109
-29
lines changed

9 files changed

+109
-29
lines changed

Documentation/config/credential.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ credential.useHttpPath::
1414
or https URL to be important. Defaults to false. See
1515
linkgit:gitcredentials[7] for more information.
1616

17+
credential.sanitizePrompt::
18+
By default, user names and hosts that are shown as part of the
19+
password prompt are not allowed to contain control characters (they
20+
will be URL-encoded by default). Configure this setting to `false` to
21+
override that behavior.
22+
23+
credential.protectProtocol::
24+
By default, Carriage Return characters are not allowed in the protocol
25+
that is used when Git talks to a credential helper. This setting allows
26+
users to override this default.
27+
1728
credential.username::
1829
If no username is set for a network authentication, use this username
1930
by default. See credential.<context>.* below, and

credential.c

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ static int credential_config_callback(const char *var, const char *value,
6767
}
6868
else if (!strcmp(key, "usehttppath"))
6969
c->use_http_path = git_config_bool(var, value);
70+
else if (!strcmp(key, "sanitizeprompt"))
71+
c->sanitize_prompt = git_config_bool(var, value);
72+
else if (!strcmp(key, "protectprotocol"))
73+
c->protect_protocol = git_config_bool(var, value);
7074

7175
return 0;
7276
}
@@ -164,7 +168,8 @@ static void credential_format(struct credential *c, struct strbuf *out)
164168
strbuf_addch(out, '@');
165169
}
166170
if (c->host)
167-
strbuf_addstr(out, c->host);
171+
strbuf_add_percentencode(out, c->host,
172+
STRBUF_ENCODE_HOST_AND_PORT);
168173
if (c->path) {
169174
strbuf_addch(out, '/');
170175
strbuf_add_percentencode(out, c->path, 0);
@@ -178,7 +183,10 @@ static char *credential_ask_one(const char *what, struct credential *c,
178183
struct strbuf prompt = STRBUF_INIT;
179184
char *r;
180185

181-
credential_describe(c, &desc);
186+
if (c->sanitize_prompt)
187+
credential_format(c, &desc);
188+
else
189+
credential_describe(c, &desc);
182190
if (desc.len)
183191
strbuf_addf(&prompt, "%s for '%s': ", what, desc.buf);
184192
else
@@ -256,7 +264,8 @@ int credential_read(struct credential *c, FILE *fp)
256264
return 0;
257265
}
258266

259-
static void credential_write_item(FILE *fp, const char *key, const char *value,
267+
static void credential_write_item(const struct credential *c,
268+
FILE *fp, const char *key, const char *value,
260269
int required)
261270
{
262271
if (!value && required)
@@ -265,19 +274,23 @@ static void credential_write_item(FILE *fp, const char *key, const char *value,
265274
return;
266275
if (strchr(value, '\n'))
267276
die("credential value for %s contains newline", key);
277+
if (c->protect_protocol && strchr(value, '\r'))
278+
die("credential value for %s contains carriage return\n"
279+
"If this is intended, set `credential.protectProtocol=false`",
280+
key);
268281
fprintf(fp, "%s=%s\n", key, value);
269282
}
270283

271284
void credential_write(const struct credential *c, FILE *fp)
272285
{
273-
credential_write_item(fp, "protocol", c->protocol, 1);
274-
credential_write_item(fp, "host", c->host, 1);
275-
credential_write_item(fp, "path", c->path, 0);
276-
credential_write_item(fp, "username", c->username, 0);
277-
credential_write_item(fp, "password", c->password, 0);
286+
credential_write_item(c, fp, "protocol", c->protocol, 1);
287+
credential_write_item(c, fp, "host", c->host, 1);
288+
credential_write_item(c, fp, "path", c->path, 0);
289+
credential_write_item(c, fp, "username", c->username, 0);
290+
credential_write_item(c, fp, "password", c->password, 0);
278291
if (c->password_expiry_utc != TIME_MAX) {
279292
char *s = xstrfmt("%"PRItime, c->password_expiry_utc);
280-
credential_write_item(fp, "password_expiry_utc", s, 0);
293+
credential_write_item(c, fp, "password_expiry_utc", s, 0);
281294
free(s);
282295
}
283296
}

credential.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ struct credential {
119119
configured:1,
120120
quit:1,
121121
use_http_path:1,
122-
username_from_proto:1;
122+
username_from_proto:1,
123+
sanitize_prompt:1,
124+
protect_protocol:1;
123125

124126
char *username;
125127
char *password;
@@ -132,6 +134,8 @@ struct credential {
132134
#define CREDENTIAL_INIT { \
133135
.helpers = STRING_LIST_INIT_DUP, \
134136
.password_expiry_utc = TIME_MAX, \
137+
.sanitize_prompt = 1, \
138+
.protect_protocol = 1, \
135139
}
136140

137141
/* 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
@@ -492,7 +492,9 @@ void strbuf_add_percentencode(struct strbuf *dst, const char *src, int flags)
492492
unsigned char ch = src[i];
493493
if (ch <= 0x1F || ch >= 0x7F ||
494494
(ch == '/' && (flags & STRBUF_ENCODE_SLASH)) ||
495-
strchr(URL_UNSAFE_CHARS, ch))
495+
((flags & STRBUF_ENCODE_HOST_AND_PORT) ?
496+
!isalnum(ch) && !strchr("-.:[]", ch) :
497+
!!strchr(URL_UNSAFE_CHARS, ch)))
496498
strbuf_addf(dst, "%%%02X", (unsigned char)ch);
497499
else
498500
strbuf_addch(dst, ch);

strbuf.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ size_t strbuf_expand_dict_cb(struct strbuf *sb,
380380
void strbuf_addbuf_percentquote(struct strbuf *dst, const struct strbuf *src);
381381

382382
#define STRBUF_ENCODE_SLASH 1
383+
#define STRBUF_ENCODE_HOST_AND_PORT 2
383384

384385
/**
385386
* 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
@@ -45,6 +45,10 @@ test_expect_success 'setup helper scripts' '
4545
test -z "$pexpiry" || echo password_expiry_utc=$pexpiry
4646
EOF
4747
48+
write_script git-credential-cntrl-in-username <<-\EOF &&
49+
printf "username=\\007latrix Lestrange\\n"
50+
EOF
51+
4852
PATH="$PWD:$PATH"
4953
'
5054

@@ -514,6 +518,19 @@ test_expect_success 'match percent-encoded values in username' '
514518
EOF
515519
'
516520

521+
test_expect_success 'match percent-encoded values in hostname' '
522+
test_config "credential.https://a%20b%20c/.helper" "$HELPER" &&
523+
check fill <<-\EOF
524+
url=https://a b c/
525+
--
526+
protocol=https
527+
host=a b c
528+
username=foo
529+
password=bar
530+
--
531+
EOF
532+
'
533+
517534
test_expect_success 'fetch with multiple path components' '
518535
test_unconfig credential.helper &&
519536
test_config credential.https://example.com/foo/repo.git.helper "verbatim foo bar" &&
@@ -703,6 +720,22 @@ test_expect_success 'url parser rejects embedded newlines' '
703720
test_cmp expect stderr
704721
'
705722

723+
test_expect_success 'url parser rejects embedded carriage returns' '
724+
test_config credential.helper "!true" &&
725+
test_must_fail git credential fill 2>stderr <<-\EOF &&
726+
url=https://example%0d.com/
727+
EOF
728+
cat >expect <<-\EOF &&
729+
fatal: credential value for host contains carriage return
730+
If this is intended, set `credential.protectProtocol=false`
731+
EOF
732+
test_cmp expect stderr &&
733+
GIT_ASKPASS=true \
734+
git -c credential.protectProtocol=false credential fill <<-\EOF
735+
url=https://example%0d.com/
736+
EOF
737+
'
738+
706739
test_expect_success 'host-less URLs are parsed as empty host' '
707740
check fill "verbatim foo bar" <<-\EOF
708741
url=cert:///path/to/cert.pem
@@ -812,4 +845,20 @@ test_expect_success 'credential config with partial URLs' '
812845
test_i18ngrep "skipping credential lookup for key" stderr
813846
'
814847

848+
BEL="$(printf '\007')"
849+
850+
test_expect_success 'interactive prompt is sanitized' '
851+
check fill cntrl-in-username <<-EOF
852+
protocol=https
853+
host=example.org
854+
--
855+
protocol=https
856+
host=example.org
857+
username=${BEL}latrix Lestrange
858+
password=askpass-password
859+
--
860+
askpass: Password for ${SQ}https://%07latrix%[email protected]${SQ}:
861+
EOF
862+
'
863+
815864
test_done

t/t5541-http-push-smart.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ test_expect_success 'push over smart http with auth' '
351351
git push "$HTTPD_URL"/auth/smart/test_repo.git &&
352352
git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/test_repo.git" \
353353
log -1 --format=%s >actual &&
354-
expect_askpass both user@host &&
354+
expect_askpass both user%40host &&
355355
test_cmp expect actual
356356
'
357357

@@ -363,7 +363,7 @@ test_expect_success 'push to auth-only-for-push repo' '
363363
git push "$HTTPD_URL"/auth-push/smart/test_repo.git &&
364364
git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/test_repo.git" \
365365
log -1 --format=%s >actual &&
366-
expect_askpass both user@host &&
366+
expect_askpass both user%40host &&
367367
test_cmp expect actual
368368
'
369369

@@ -393,7 +393,7 @@ test_expect_success 'push into half-auth-complete requires password' '
393393
git push "$HTTPD_URL/half-auth-complete/smart/half-auth.git" &&
394394
git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/half-auth.git" \
395395
log -1 --format=%s >actual &&
396-
expect_askpass both user@host &&
396+
expect_askpass both user%40host &&
397397
test_cmp expect actual
398398
'
399399

t/t5550-http-fetch-dumb.sh

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,13 @@ test_expect_success 'http auth can use user/pass in URL' '
9090
test_expect_success 'http auth can use just user in URL' '
9191
set_askpass wrong pass@host &&
9292
git clone "$HTTPD_URL_USER/auth/dumb/repo.git" clone-auth-pass &&
93-
expect_askpass pass user@host
93+
expect_askpass pass user%40host
9494
'
9595

9696
test_expect_success 'http auth can request both user and pass' '
9797
set_askpass user@host pass@host &&
9898
git clone "$HTTPD_URL/auth/dumb/repo.git" clone-auth-both &&
99-
expect_askpass both user@host
99+
expect_askpass both user%40host
100100
'
101101

102102
test_expect_success 'http auth respects credential helper config' '
@@ -114,14 +114,14 @@ test_expect_success 'http auth can get username from config' '
114114
test_config_global "credential.$HTTPD_URL.username" user@host &&
115115
set_askpass wrong pass@host &&
116116
git clone "$HTTPD_URL/auth/dumb/repo.git" clone-auth-user &&
117-
expect_askpass pass user@host
117+
expect_askpass pass user%40host
118118
'
119119

120120
test_expect_success 'configured username does not override URL' '
121121
test_config_global "credential.$HTTPD_URL.username" wrong &&
122122
set_askpass wrong pass@host &&
123123
git clone "$HTTPD_URL_USER/auth/dumb/repo.git" clone-auth-user2 &&
124-
expect_askpass pass user@host
124+
expect_askpass pass user%40host
125125
'
126126

127127
test_expect_success 'set up repo with http submodules' '
@@ -142,7 +142,7 @@ test_expect_success 'cmdline credential config passes to submodule via clone' '
142142
set_askpass wrong pass@host &&
143143
git -c "credential.$HTTPD_URL.username=user@host" \
144144
clone --recursive super super-clone &&
145-
expect_askpass pass user@host
145+
expect_askpass pass user%40host
146146
'
147147

148148
test_expect_success 'cmdline credential config passes submodule via fetch' '
@@ -153,7 +153,7 @@ test_expect_success 'cmdline credential config passes submodule via fetch' '
153153
git -C super-clone \
154154
-c "credential.$HTTPD_URL.username=user@host" \
155155
fetch --recurse-submodules &&
156-
expect_askpass pass user@host
156+
expect_askpass pass user%40host
157157
'
158158

159159
test_expect_success 'cmdline credential config passes submodule update' '
@@ -170,7 +170,7 @@ test_expect_success 'cmdline credential config passes submodule update' '
170170
git -C super-clone \
171171
-c "credential.$HTTPD_URL.username=user@host" \
172172
submodule update &&
173-
expect_askpass pass user@host
173+
expect_askpass pass user%40host
174174
'
175175

176176
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
'
@@ -199,7 +199,7 @@ test_expect_success 'clone from auth-only-for-objects repository' '
199199
echo two >expect &&
200200
set_askpass user@host pass@host &&
201201
git clone --bare "$HTTPD_URL/auth-fetch/smart/repo.git" half-auth &&
202-
expect_askpass both user@host &&
202+
expect_askpass both user%40host &&
203203
git --git-dir=half-auth log -1 --format=%s >actual &&
204204
test_cmp expect actual
205205
'
@@ -224,14 +224,14 @@ test_expect_success 'redirects send auth to new location' '
224224
set_askpass user@host pass@host &&
225225
git -c credential.useHttpPath=true \
226226
clone $HTTPD_URL/smart-redir-auth/repo.git repo-redir-auth &&
227-
expect_askpass both user@host auth/smart/repo.git
227+
expect_askpass both user%40host auth/smart/repo.git
228228
'
229229

230230
test_expect_success 'GIT_TRACE_CURL redacts auth details' '
231231
rm -rf redact-auth trace &&
232232
set_askpass user@host pass@host &&
233233
GIT_TRACE_CURL="$(pwd)/trace" git clone --bare "$HTTPD_URL/auth/smart/repo.git" redact-auth &&
234-
expect_askpass both user@host &&
234+
expect_askpass both user%40host &&
235235
236236
# Ensure that there is no "Basic" followed by a base64 string, but that
237237
# the auth details are redacted
@@ -243,7 +243,7 @@ test_expect_success 'GIT_CURL_VERBOSE redacts auth details' '
243243
rm -rf redact-auth trace &&
244244
set_askpass user@host pass@host &&
245245
GIT_CURL_VERBOSE=1 git clone --bare "$HTTPD_URL/auth/smart/repo.git" redact-auth 2>trace &&
246-
expect_askpass both user@host &&
246+
expect_askpass both user%40host &&
247247
248248
# Ensure that there is no "Basic" followed by a base64 string, but that
249249
# the auth details are redacted
@@ -256,7 +256,7 @@ test_expect_success 'GIT_TRACE_CURL does not redact auth details if GIT_TRACE_RE
256256
set_askpass user@host pass@host &&
257257
GIT_TRACE_REDACT=0 GIT_TRACE_CURL="$(pwd)/trace" \
258258
git clone --bare "$HTTPD_URL/auth/smart/repo.git" redact-auth &&
259-
expect_askpass both user@host &&
259+
expect_askpass both user%40host &&
260260
261261
grep -i "Authorization: Basic [0-9a-zA-Z+/]" trace
262262
'
@@ -568,7 +568,7 @@ test_expect_success 'http auth remembers successful credentials' '
568568
# the first request prompts the user...
569569
set_askpass user@host pass@host &&
570570
git ls-remote "$HTTPD_URL/auth/smart/repo.git" >/dev/null &&
571-
expect_askpass both user@host &&
571+
expect_askpass both user%40host &&
572572
573573
# ...and the second one uses the stored value rather than
574574
# prompting the user.
@@ -599,7 +599,7 @@ test_expect_success 'http auth forgets bogus credentials' '
599599
# us to prompt the user again.
600600
set_askpass user@host pass@host &&
601601
git ls-remote "$HTTPD_URL/auth/smart/repo.git" >/dev/null &&
602-
expect_askpass both user@host
602+
expect_askpass both user%40host
603603
'
604604

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

0 commit comments

Comments
 (0)