]> git.ipfire.org Git - thirdparty/git.git/commitdiff
credential: disallow Carriage Returns in the protocol by default
authorJohannes Schindelin <johannes.schindelin@gmx.de>
Mon, 4 Nov 2024 13:48:22 +0000 (14:48 +0100)
committerJohannes Schindelin <johannes.schindelin@gmx.de>
Tue, 26 Nov 2024 19:24:04 +0000 (20:24 +0100)
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 <johannes.schindelin@gmx.de>
Documentation/config/credential.txt
credential.c
credential.h
t/t0300-credentials.sh

index fd8113d6d42f758e8e748eb7b4251411ebf84ccd..9cadca7f73db67a86cefe8a49040947b6b13826b 100644 (file)
@@ -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
index 1392a54d5c2c3ef608dc56851774125709f9dd32..b76a730901f5ea375670bc3f182394caed4d908d 100644 (file)
@@ -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;
 }
@@ -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)
@@ -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);
        }
 }
index 0364d436d242d6aec041ddd04ba5ea85ee0e9f75..2c0b39a925405fae99859d14f45da6e1c4e4d0cd 100644 (file)
@@ -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;
@@ -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. */
index b62c70c193eff30f5ee342697bdea0d95437cfa9..168ae76550155da1acd6b7ad397686737e4b64b1 100755 (executable)
@@ -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