Skip to content

Commit 429023c

Browse files
committed
credential: disallow Carriage Returns in the protocol by default
While Git has documented that the credential protocol is line-based, with newlines as terminators, the exact shape of a newline has not been documented. From Git's perspective, which is firmly rooted in the Linux ecosystem, it is clear that "a newline" means a Line Feed character. However, even Git's credential protocol respects Windows line endings (a Carriage Return character followed by a Line Feed character, "CR/LF") by virtue of using `strbuf_getline()`. There is a third category of line endings that has been used originally by MacOS, and that is respected by the default line readers of .NET and node.js: bare Carriage Returns. Git cannot handle those, and what is worse: Git's remedy against CVE-2020-5260 does not catch when credential helpers are used that interpret bare Carriage Returns as newlines. Git Credential Manager addressed this as CVE-2024-50338, but other credential helpers may still be vulnerable. So let's not only disallow Line Feed characters as part of the values in the credential protocol, but also disallow Carriage Return characters. In the unlikely event that a credential helper relies on Carriage Returns in the protocol, introduce an escape hatch via the `credential.protectProtocol` config setting. This addresses CVE-2024-52006. Signed-off-by: Johannes Schindelin <[email protected]>
1 parent db58126 commit 429023c

File tree

4 files changed

+47
-17
lines changed

4 files changed

+47
-17
lines changed

Documentation/config/credential.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ credential.sanitizePrompt::
2828
will be URL-encoded by default). Configure this setting to `false` to
2929
override that behavior.
3030

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+
3136
credential.username::
3237
If no username is set for a network authentication, use this username
3338
by default. See credential.<context>.* below, and

credential.c

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ static int credential_config_callback(const char *var, const char *value,
132132
c->use_http_path = git_config_bool(var, value);
133133
else if (!strcmp(key, "sanitizeprompt"))
134134
c->sanitize_prompt = git_config_bool(var, value);
135+
else if (!strcmp(key, "protectprotocol"))
136+
c->protect_protocol = git_config_bool(var, value);
135137

136138
return 0;
137139
}
@@ -388,7 +390,8 @@ int credential_read(struct credential *c, FILE *fp,
388390
return 0;
389391
}
390392

391-
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,
392395
int required)
393396
{
394397
if (!value && required)
@@ -397,41 +400,45 @@ static void credential_write_item(FILE *fp, const char *key, const char *value,
397400
return;
398401
if (strchr(value, '\n'))
399402
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);
400407
fprintf(fp, "%s=%s\n", key, value);
401408
}
402409

403410
void credential_write(const struct credential *c, FILE *fp,
404411
enum credential_op_type op_type)
405412
{
406413
if (credential_has_capability(&c->capa_authtype, op_type))
407-
credential_write_item(fp, "capability[]", "authtype", 0);
414+
credential_write_item(c, fp, "capability[]", "authtype", 0);
408415
if (credential_has_capability(&c->capa_state, op_type))
409-
credential_write_item(fp, "capability[]", "state", 0);
416+
credential_write_item(c, fp, "capability[]", "state", 0);
410417

411418
if (credential_has_capability(&c->capa_authtype, op_type)) {
412-
credential_write_item(fp, "authtype", c->authtype, 0);
413-
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);
414421
if (c->ephemeral)
415-
credential_write_item(fp, "ephemeral", "1", 0);
422+
credential_write_item(c, fp, "ephemeral", "1", 0);
416423
}
417-
credential_write_item(fp, "protocol", c->protocol, 1);
418-
credential_write_item(fp, "host", c->host, 1);
419-
credential_write_item(fp, "path", c->path, 0);
420-
credential_write_item(fp, "username", c->username, 0);
421-
credential_write_item(fp, "password", c->password, 0);
422-
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);
423430
if (c->password_expiry_utc != TIME_MAX) {
424431
char *s = xstrfmt("%"PRItime, c->password_expiry_utc);
425-
credential_write_item(fp, "password_expiry_utc", s, 0);
432+
credential_write_item(c, fp, "password_expiry_utc", s, 0);
426433
free(s);
427434
}
428435
for (size_t i = 0; i < c->wwwauth_headers.nr; i++)
429-
credential_write_item(fp, "wwwauth[]", c->wwwauth_headers.v[i], 0);
436+
credential_write_item(c, fp, "wwwauth[]", c->wwwauth_headers.v[i], 0);
430437
if (credential_has_capability(&c->capa_state, op_type)) {
431438
if (c->multistage)
432-
credential_write_item(fp, "continue", "1", 0);
439+
credential_write_item(c, fp, "continue", "1", 0);
433440
for (size_t i = 0; i < c->state_headers_to_send.nr; i++)
434-
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);
435442
}
436443
}
437444

credential.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ struct credential {
169169
quit:1,
170170
use_http_path:1,
171171
username_from_proto:1,
172-
sanitize_prompt:1;
172+
sanitize_prompt:1,
173+
protect_protocol:1;
173174

174175
struct credential_capability capa_authtype;
175176
struct credential_capability capa_state;
@@ -197,6 +198,7 @@ struct credential {
197198
.state_headers = STRVEC_INIT, \
198199
.state_headers_to_send = STRVEC_INIT, \
199200
.sanitize_prompt = 1, \
201+
.protect_protocol = 1, \
200202
}
201203

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

t/t0300-credentials.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,22 @@ test_expect_success 'url parser rejects embedded newlines' '
902902
test_cmp expect stderr
903903
'
904904

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+
905921
test_expect_success 'host-less URLs are parsed as empty host' '
906922
check fill "verbatim foo bar" <<-\EOF
907923
url=cert:///path/to/cert.pem

0 commit comments

Comments
 (0)