Skip to content

Commit 5d50f4e

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 a8aef7f + b01b9b8 commit 5d50f4e

9 files changed

+111
-31
lines changed

Documentation/config/credential.txt

+11
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

+24-11
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ static int credential_config_callback(const char *var, const char *value,
7474
}
7575
else if (!strcmp(key, "usehttppath"))
7676
c->use_http_path = git_config_bool(var, value);
77+
else if (!strcmp(key, "sanitizeprompt"))
78+
c->sanitize_prompt = git_config_bool(var, value);
79+
else if (!strcmp(key, "protectprotocol"))
80+
c->protect_protocol = git_config_bool(var, value);
7781

7882
return 0;
7983
}
@@ -171,7 +175,8 @@ static void credential_format(struct credential *c, struct strbuf *out)
171175
strbuf_addch(out, '@');
172176
}
173177
if (c->host)
174-
strbuf_addstr(out, c->host);
178+
strbuf_add_percentencode(out, c->host,
179+
STRBUF_ENCODE_HOST_AND_PORT);
175180
if (c->path) {
176181
strbuf_addch(out, '/');
177182
strbuf_add_percentencode(out, c->path, 0);
@@ -185,7 +190,10 @@ static char *credential_ask_one(const char *what, struct credential *c,
185190
struct strbuf prompt = STRBUF_INIT;
186191
char *r;
187192

188-
credential_describe(c, &desc);
193+
if (c->sanitize_prompt)
194+
credential_format(c, &desc);
195+
else
196+
credential_describe(c, &desc);
189197
if (desc.len)
190198
strbuf_addf(&prompt, "%s for '%s': ", what, desc.buf);
191199
else
@@ -268,7 +276,8 @@ int credential_read(struct credential *c, FILE *fp)
268276
return 0;
269277
}
270278

271-
static void credential_write_item(FILE *fp, const char *key, const char *value,
279+
static void credential_write_item(const struct credential *c,
280+
FILE *fp, const char *key, const char *value,
272281
int required)
273282
{
274283
if (!value && required)
@@ -277,24 +286,28 @@ static void credential_write_item(FILE *fp, const char *key, const char *value,
277286
return;
278287
if (strchr(value, '\n'))
279288
die("credential value for %s contains newline", key);
289+
if (c->protect_protocol && strchr(value, '\r'))
290+
die("credential value for %s contains carriage return\n"
291+
"If this is intended, set `credential.protectProtocol=false`",
292+
key);
280293
fprintf(fp, "%s=%s\n", key, value);
281294
}
282295

283296
void credential_write(const struct credential *c, FILE *fp)
284297
{
285-
credential_write_item(fp, "protocol", c->protocol, 1);
286-
credential_write_item(fp, "host", c->host, 1);
287-
credential_write_item(fp, "path", c->path, 0);
288-
credential_write_item(fp, "username", c->username, 0);
289-
credential_write_item(fp, "password", c->password, 0);
290-
credential_write_item(fp, "oauth_refresh_token", c->oauth_refresh_token, 0);
298+
credential_write_item(c, fp, "protocol", c->protocol, 1);
299+
credential_write_item(c, fp, "host", c->host, 1);
300+
credential_write_item(c, fp, "path", c->path, 0);
301+
credential_write_item(c, fp, "username", c->username, 0);
302+
credential_write_item(c, fp, "password", c->password, 0);
303+
credential_write_item(c, fp, "oauth_refresh_token", c->oauth_refresh_token, 0);
291304
if (c->password_expiry_utc != TIME_MAX) {
292305
char *s = xstrfmt("%"PRItime, c->password_expiry_utc);
293-
credential_write_item(fp, "password_expiry_utc", s, 0);
306+
credential_write_item(c, fp, "password_expiry_utc", s, 0);
294307
free(s);
295308
}
296309
for (size_t i = 0; i < c->wwwauth_headers.nr; i++)
297-
credential_write_item(fp, "wwwauth[]", c->wwwauth_headers.v[i], 0);
310+
credential_write_item(c, fp, "wwwauth[]", c->wwwauth_headers.v[i], 0);
298311
}
299312

300313
static int run_credential_helper(struct credential *c,

credential.h

+5-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@ struct credential {
134134
configured:1,
135135
quit:1,
136136
use_http_path:1,
137-
username_from_proto:1;
137+
username_from_proto:1,
138+
sanitize_prompt:1,
139+
protect_protocol:1;
138140

139141
char *username;
140142
char *password;
@@ -149,6 +151,8 @@ struct credential {
149151
.helpers = STRING_LIST_INIT_DUP, \
150152
.password_expiry_utc = TIME_MAX, \
151153
.wwwauth_headers = STRVEC_INIT, \
154+
.sanitize_prompt = 1, \
155+
.protect_protocol = 1, \
152156
}
153157

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

strbuf.c

+3-1
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,9 @@ void strbuf_add_percentencode(struct strbuf *dst, const char *src, int flags)
486486
unsigned char ch = src[i];
487487
if (ch <= 0x1F || ch >= 0x7F ||
488488
(ch == '/' && (flags & STRBUF_ENCODE_SLASH)) ||
489-
strchr(URL_UNSAFE_CHARS, ch))
489+
((flags & STRBUF_ENCODE_HOST_AND_PORT) ?
490+
!isalnum(ch) && !strchr("-.:[]", ch) :
491+
!!strchr(URL_UNSAFE_CHARS, ch)))
490492
strbuf_addf(dst, "%%%02X", (unsigned char)ch);
491493
else
492494
strbuf_addch(dst, ch);

strbuf.h

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

353353
#define STRBUF_ENCODE_SLASH 1
354+
#define STRBUF_ENCODE_HOST_AND_PORT 2
354355

355356
/**
356357
* 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
@@ -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_SEP$PATH"
4953
'
5054

@@ -532,6 +536,19 @@ test_expect_success 'match percent-encoded values in username' '
532536
EOF
533537
'
534538

539+
test_expect_success 'match percent-encoded values in hostname' '
540+
test_config "credential.https://a%20b%20c/.helper" "$HELPER" &&
541+
check fill <<-\EOF
542+
url=https://a b c/
543+
--
544+
protocol=https
545+
host=a b c
546+
username=foo
547+
password=bar
548+
--
549+
EOF
550+
'
551+
535552
test_expect_success 'fetch with multiple path components' '
536553
test_unconfig credential.helper &&
537554
test_config credential.https://example.com/foo/repo.git.helper "verbatim foo bar" &&
@@ -721,6 +738,22 @@ test_expect_success 'url parser rejects embedded newlines' '
721738
test_cmp expect stderr
722739
'
723740

741+
test_expect_success 'url parser rejects embedded carriage returns' '
742+
test_config credential.helper "!true" &&
743+
test_must_fail git credential fill 2>stderr <<-\EOF &&
744+
url=https://example%0d.com/
745+
EOF
746+
cat >expect <<-\EOF &&
747+
fatal: credential value for host contains carriage return
748+
If this is intended, set `credential.protectProtocol=false`
749+
EOF
750+
test_cmp expect stderr &&
751+
GIT_ASKPASS=true \
752+
git -c credential.protectProtocol=false credential fill <<-\EOF
753+
url=https://example%0d.com/
754+
EOF
755+
'
756+
724757
test_expect_success 'host-less URLs are parsed as empty host' '
725758
check fill "verbatim foo bar" <<-\EOF
726759
url=cert:///path/to/cert.pem
@@ -830,4 +863,20 @@ test_expect_success 'credential config with partial URLs' '
830863
test_grep "skipping credential lookup for key" stderr
831864
'
832865

866+
BEL="$(printf '\007')"
867+
868+
test_expect_success 'interactive prompt is sanitized' '
869+
check fill cntrl-in-username <<-EOF
870+
protocol=https
871+
host=example.org
872+
--
873+
protocol=https
874+
host=example.org
875+
username=${BEL}latrix Lestrange
876+
password=askpass-password
877+
--
878+
askpass: Password for ${SQ}https://%07latrix%[email protected]${SQ}:
879+
EOF
880+
'
881+
833882
test_done

t/t5541-http-push-smart.sh

+3-3
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

+7-7
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

+8-8
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
'
@@ -570,7 +570,7 @@ test_expect_success 'http auth remembers successful credentials' '
570570
# the first request prompts the user...
571571
set_askpass user@host pass@host &&
572572
git ls-remote "$HTTPD_URL/auth/smart/repo.git" >/dev/null &&
573-
expect_askpass both user@host &&
573+
expect_askpass both user%40host &&
574574
575575
# ...and the second one uses the stored value rather than
576576
# prompting the user.
@@ -601,7 +601,7 @@ test_expect_success 'http auth forgets bogus credentials' '
601601
# us to prompt the user again.
602602
set_askpass user@host pass@host &&
603603
git ls-remote "$HTTPD_URL/auth/smart/repo.git" >/dev/null &&
604-
expect_askpass both user@host
604+
expect_askpass both user%40host
605605
'
606606

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

0 commit comments

Comments
 (0)