Skip to content

Commit b01b9b8

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 7725b81 commit b01b9b8

File tree

4 files changed

+38
-8
lines changed

4 files changed

+38
-8
lines changed

Documentation/config/credential.txt

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

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

credential.c

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ static int credential_config_callback(const char *var, const char *value,
6969
c->use_http_path = git_config_bool(var, value);
7070
else if (!strcmp(key, "sanitizeprompt"))
7171
c->sanitize_prompt = git_config_bool(var, value);
72+
else if (!strcmp(key, "protectprotocol"))
73+
c->protect_protocol = git_config_bool(var, value);
7274

7375
return 0;
7476
}
@@ -262,7 +264,8 @@ int credential_read(struct credential *c, FILE *fp)
262264
return 0;
263265
}
264266

265-
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,
266269
int required)
267270
{
268271
if (!value && required)
@@ -271,19 +274,23 @@ static void credential_write_item(FILE *fp, const char *key, const char *value,
271274
return;
272275
if (strchr(value, '\n'))
273276
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);
274281
fprintf(fp, "%s=%s\n", key, value);
275282
}
276283

277284
void credential_write(const struct credential *c, FILE *fp)
278285
{
279-
credential_write_item(fp, "protocol", c->protocol, 1);
280-
credential_write_item(fp, "host", c->host, 1);
281-
credential_write_item(fp, "path", c->path, 0);
282-
credential_write_item(fp, "username", c->username, 0);
283-
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);
284291
if (c->password_expiry_utc != TIME_MAX) {
285292
char *s = xstrfmt("%"PRItime, c->password_expiry_utc);
286-
credential_write_item(fp, "password_expiry_utc", s, 0);
293+
credential_write_item(c, fp, "password_expiry_utc", s, 0);
287294
free(s);
288295
}
289296
}

credential.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ struct credential {
120120
quit:1,
121121
use_http_path:1,
122122
username_from_proto:1,
123-
sanitize_prompt:1;
123+
sanitize_prompt:1,
124+
protect_protocol:1;
124125

125126
char *username;
126127
char *password;
@@ -134,6 +135,7 @@ struct credential {
134135
.helpers = STRING_LIST_INIT_DUP, \
135136
.password_expiry_utc = TIME_MAX, \
136137
.sanitize_prompt = 1, \
138+
.protect_protocol = 1, \
137139
}
138140

139141
/* 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
@@ -720,6 +720,22 @@ test_expect_success 'url parser rejects embedded newlines' '
720720
test_cmp expect stderr
721721
'
722722

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

0 commit comments

Comments
 (0)