Skip to content

Commit

Permalink
credential: disallow Carriage Returns in the protocol by default
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
dscho committed Nov 26, 2024
1 parent 7725b81 commit b01b9b8
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 8 deletions.
5 changes: 5 additions & 0 deletions Documentation/config/credential.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ credential.sanitizePrompt::
will be URL-encoded by default). Configure this setting to `false` to
override that behavior.

credential.protectProtocol::
By default, Carriage Return characters are not allowed in the protocol
that is used when Git talks to a credential helper. This setting allows
users to override this default.

credential.username::
If no username is set for a network authentication, use this username
by default. See credential.<context>.* below, and
Expand Down
21 changes: 14 additions & 7 deletions credential.c
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ static int credential_config_callback(const char *var, const char *value,
c->use_http_path = git_config_bool(var, value);
else if (!strcmp(key, "sanitizeprompt"))
c->sanitize_prompt = git_config_bool(var, value);
else if (!strcmp(key, "protectprotocol"))
c->protect_protocol = git_config_bool(var, value);

return 0;
}
Expand Down Expand Up @@ -262,7 +264,8 @@ int credential_read(struct credential *c, FILE *fp)
return 0;
}

static void credential_write_item(FILE *fp, const char *key, const char *value,
static void credential_write_item(const struct credential *c,
FILE *fp, const char *key, const char *value,
int required)
{
if (!value && required)
Expand All @@ -271,19 +274,23 @@ static void credential_write_item(FILE *fp, const char *key, const char *value,
return;
if (strchr(value, '\n'))
die("credential value for %s contains newline", key);
if (c->protect_protocol && strchr(value, '\r'))
die("credential value for %s contains carriage return\n"
"If this is intended, set `credential.protectProtocol=false`",
key);
fprintf(fp, "%s=%s\n", key, value);
}

void credential_write(const struct credential *c, FILE *fp)
{
credential_write_item(fp, "protocol", c->protocol, 1);
credential_write_item(fp, "host", c->host, 1);
credential_write_item(fp, "path", c->path, 0);
credential_write_item(fp, "username", c->username, 0);
credential_write_item(fp, "password", c->password, 0);
credential_write_item(c, fp, "protocol", c->protocol, 1);
credential_write_item(c, fp, "host", c->host, 1);
credential_write_item(c, fp, "path", c->path, 0);
credential_write_item(c, fp, "username", c->username, 0);
credential_write_item(c, fp, "password", c->password, 0);
if (c->password_expiry_utc != TIME_MAX) {
char *s = xstrfmt("%"PRItime, c->password_expiry_utc);
credential_write_item(fp, "password_expiry_utc", s, 0);
credential_write_item(c, fp, "password_expiry_utc", s, 0);
free(s);
}
}
Expand Down
4 changes: 3 additions & 1 deletion credential.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ struct credential {
quit:1,
use_http_path:1,
username_from_proto:1,
sanitize_prompt:1;
sanitize_prompt:1,
protect_protocol:1;

char *username;
char *password;
Expand All @@ -134,6 +135,7 @@ struct credential {
.helpers = STRING_LIST_INIT_DUP, \
.password_expiry_utc = TIME_MAX, \
.sanitize_prompt = 1, \
.protect_protocol = 1, \
}

/* Initialize a credential structure, setting all fields to empty. */
Expand Down
16 changes: 16 additions & 0 deletions t/t0300-credentials.sh
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,22 @@ test_expect_success 'url parser rejects embedded newlines' '
test_cmp expect stderr
'

test_expect_success 'url parser rejects embedded carriage returns' '
test_config credential.helper "!true" &&
test_must_fail git credential fill 2>stderr <<-\EOF &&
url=https://example%0d.com/
EOF
cat >expect <<-\EOF &&
fatal: credential value for host contains carriage return
If this is intended, set `credential.protectProtocol=false`
EOF
test_cmp expect stderr &&
GIT_ASKPASS=true \
git -c credential.protectProtocol=false credential fill <<-\EOF
url=https://example%0d.com/
EOF
'

test_expect_success 'host-less URLs are parsed as empty host' '
check fill "verbatim foo bar" <<-\EOF
url=cert:///path/to/cert.pem
Expand Down

0 comments on commit b01b9b8

Please sign in to comment.