From b01b9b81d36759cdcd07305e78765199e1bc2060 Mon Sep 17 00:00:00 2001
From: Johannes Schindelin <johannes.schindelin@gmx.de>
Date: Mon, 4 Nov 2024 14:48:22 +0100
Subject: [PATCH] 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 <johannes.schindelin@gmx.de>
---
 Documentation/config/credential.txt |  5 +++++
 credential.c                        | 21 ++++++++++++++-------
 credential.h                        |  4 +++-
 t/t0300-credentials.sh              | 16 ++++++++++++++++
 4 files changed, 38 insertions(+), 8 deletions(-)

diff --git a/Documentation/config/credential.txt b/Documentation/config/credential.txt
index fd8113d6..9cadca7f 100644
--- a/Documentation/config/credential.txt
+++ b/Documentation/config/credential.txt
@@ -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
diff --git a/credential.c b/credential.c
index 1392a54d..b76a7309 100644
--- a/credential.c
+++ b/credential.c
@@ -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,16 +274,20 @@ 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);
 }
 
 static int run_credential_helper(struct credential *c,
diff --git a/credential.h b/credential.h
index 0364d436..2c0b39a9 100644
--- a/credential.h
+++ b/credential.h
@@ -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 {
 #define CREDENTIAL_INIT { \
 	.helpers = STRING_LIST_INIT_DUP, \
 	.sanitize_prompt = 1, \
+	.protect_protocol = 1, \
 }
 
 /* Initialize a credential structure, setting all fields to empty. */
diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh
index b62c70c1..168ae765 100755
--- a/t/t0300-credentials.sh
+++ b/t/t0300-credentials.sh
@@ -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
-- 
2.23.0
