Skip to content

Commit

Permalink
Merge branch 'disallow-control-characters-in-credential-urls-by-default'
Browse files Browse the repository at this point in the history
This addresses two vulnerabilities:

- CVE-2024-50349:

	Printing unsanitized URLs when asking for credentials made the
	user susceptible to crafted URLs (e.g. in recursive clones) that
	mislead the user into typing in passwords for trusted sites that
	would then be sent to untrusted sites instead.

- CVE-2024-52006

	Git may pass on Carriage Returns via the credential protocol to
	credential helpers which use line-reading functions that
	interpret said Carriage Returns as line endings, even though Git
	did not intend that.

Signed-off-by: Johannes Schindelin <[email protected]>
  • Loading branch information
dscho committed Nov 26, 2024
2 parents a8aef7f + b01b9b8 commit 5d50f4e
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 31 deletions.
11 changes: 11 additions & 0 deletions Documentation/config/credential.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ credential.useHttpPath::
or https URL to be important. Defaults to false. See
linkgit:gitcredentials[7] for more information.

credential.sanitizePrompt::
By default, user names and hosts that are shown as part of the
password prompt are not allowed to contain control characters (they
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
35 changes: 24 additions & 11 deletions credential.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ static int credential_config_callback(const char *var, const char *value,
}
else if (!strcmp(key, "usehttppath"))
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 @@ -171,7 +175,8 @@ static void credential_format(struct credential *c, struct strbuf *out)
strbuf_addch(out, '@');
}
if (c->host)
strbuf_addstr(out, c->host);
strbuf_add_percentencode(out, c->host,
STRBUF_ENCODE_HOST_AND_PORT);
if (c->path) {
strbuf_addch(out, '/');
strbuf_add_percentencode(out, c->path, 0);
Expand All @@ -185,7 +190,10 @@ static char *credential_ask_one(const char *what, struct credential *c,
struct strbuf prompt = STRBUF_INIT;
char *r;

credential_describe(c, &desc);
if (c->sanitize_prompt)
credential_format(c, &desc);
else
credential_describe(c, &desc);
if (desc.len)
strbuf_addf(&prompt, "%s for '%s': ", what, desc.buf);
else
Expand Down Expand Up @@ -268,7 +276,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 @@ -277,24 +286,28 @@ 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(fp, "oauth_refresh_token", c->oauth_refresh_token, 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);
credential_write_item(c, fp, "oauth_refresh_token", c->oauth_refresh_token, 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);
}
for (size_t i = 0; i < c->wwwauth_headers.nr; i++)
credential_write_item(fp, "wwwauth[]", c->wwwauth_headers.v[i], 0);
credential_write_item(c, fp, "wwwauth[]", c->wwwauth_headers.v[i], 0);
}

static int run_credential_helper(struct credential *c,
Expand Down
6 changes: 5 additions & 1 deletion credential.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ struct credential {
configured:1,
quit:1,
use_http_path:1,
username_from_proto:1;
username_from_proto:1,
sanitize_prompt:1,
protect_protocol:1;

char *username;
char *password;
Expand All @@ -149,6 +151,8 @@ struct credential {
.helpers = STRING_LIST_INIT_DUP, \
.password_expiry_utc = TIME_MAX, \
.wwwauth_headers = STRVEC_INIT, \
.sanitize_prompt = 1, \
.protect_protocol = 1, \
}

/* Initialize a credential structure, setting all fields to empty. */
Expand Down
4 changes: 3 additions & 1 deletion strbuf.c
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,9 @@ void strbuf_add_percentencode(struct strbuf *dst, const char *src, int flags)
unsigned char ch = src[i];
if (ch <= 0x1F || ch >= 0x7F ||
(ch == '/' && (flags & STRBUF_ENCODE_SLASH)) ||
strchr(URL_UNSAFE_CHARS, ch))
((flags & STRBUF_ENCODE_HOST_AND_PORT) ?
!isalnum(ch) && !strchr("-.:[]", ch) :
!!strchr(URL_UNSAFE_CHARS, ch)))
strbuf_addf(dst, "%%%02X", (unsigned char)ch);
else
strbuf_addch(dst, ch);
Expand Down
1 change: 1 addition & 0 deletions strbuf.h
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ void strbuf_expand_bad_format(const char *format, const char *command);
void strbuf_addbuf_percentquote(struct strbuf *dst, const struct strbuf *src);

#define STRBUF_ENCODE_SLASH 1
#define STRBUF_ENCODE_HOST_AND_PORT 2

/**
* Append the contents of a string to a strbuf, percent-encoding any characters
Expand Down
49 changes: 49 additions & 0 deletions t/t0300-credentials.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ test_expect_success 'setup helper scripts' '
test -z "$pexpiry" || echo password_expiry_utc=$pexpiry
EOF
write_script git-credential-cntrl-in-username <<-\EOF &&
printf "username=\\007latrix Lestrange\\n"
EOF
PATH="$PWD$PATH_SEP$PATH"
'

Expand Down Expand Up @@ -532,6 +536,19 @@ test_expect_success 'match percent-encoded values in username' '
EOF
'

test_expect_success 'match percent-encoded values in hostname' '
test_config "credential.https://a%20b%20c/.helper" "$HELPER" &&
check fill <<-\EOF
url=https://a b c/
--
protocol=https
host=a b c
username=foo
password=bar
--
EOF
'

test_expect_success 'fetch with multiple path components' '
test_unconfig credential.helper &&
test_config credential.https://example.com/foo/repo.git.helper "verbatim foo bar" &&
Expand Down Expand Up @@ -721,6 +738,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 Expand Up @@ -830,4 +863,20 @@ test_expect_success 'credential config with partial URLs' '
test_grep "skipping credential lookup for key" stderr
'

BEL="$(printf '\007')"

test_expect_success 'interactive prompt is sanitized' '
check fill cntrl-in-username <<-EOF
protocol=https
host=example.org
--
protocol=https
host=example.org
username=${BEL}latrix Lestrange
password=askpass-password
--
askpass: Password for ${SQ}https://%07latrix%[email protected]${SQ}:
EOF
'

test_done
6 changes: 3 additions & 3 deletions t/t5541-http-push-smart.sh
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ test_expect_success 'push over smart http with auth' '
git push "$HTTPD_URL"/auth/smart/test_repo.git &&
git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/test_repo.git" \
log -1 --format=%s >actual &&
expect_askpass both user@host &&
expect_askpass both user%40host &&
test_cmp expect actual
'

Expand All @@ -355,7 +355,7 @@ test_expect_success 'push to auth-only-for-push repo' '
git push "$HTTPD_URL"/auth-push/smart/test_repo.git &&
git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/test_repo.git" \
log -1 --format=%s >actual &&
expect_askpass both user@host &&
expect_askpass both user%40host &&
test_cmp expect actual
'

Expand Down Expand Up @@ -385,7 +385,7 @@ test_expect_success 'push into half-auth-complete requires password' '
git push "$HTTPD_URL/half-auth-complete/smart/half-auth.git" &&
git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/half-auth.git" \
log -1 --format=%s >actual &&
expect_askpass both user@host &&
expect_askpass both user%40host &&
test_cmp expect actual
'

Expand Down
14 changes: 7 additions & 7 deletions t/t5550-http-fetch-dumb.sh
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ test_expect_success 'http auth can use user/pass in URL' '
test_expect_success 'http auth can use just user in URL' '
set_askpass wrong pass@host &&
git clone "$HTTPD_URL_USER/auth/dumb/repo.git" clone-auth-pass &&
expect_askpass pass user@host
expect_askpass pass user%40host
'

test_expect_success 'http auth can request both user and pass' '
set_askpass user@host pass@host &&
git clone "$HTTPD_URL/auth/dumb/repo.git" clone-auth-both &&
expect_askpass both user@host
expect_askpass both user%40host
'

test_expect_success 'http auth respects credential helper config' '
Expand All @@ -114,14 +114,14 @@ test_expect_success 'http auth can get username from config' '
test_config_global "credential.$HTTPD_URL.username" user@host &&
set_askpass wrong pass@host &&
git clone "$HTTPD_URL/auth/dumb/repo.git" clone-auth-user &&
expect_askpass pass user@host
expect_askpass pass user%40host
'

test_expect_success 'configured username does not override URL' '
test_config_global "credential.$HTTPD_URL.username" wrong &&
set_askpass wrong pass@host &&
git clone "$HTTPD_URL_USER/auth/dumb/repo.git" clone-auth-user2 &&
expect_askpass pass user@host
expect_askpass pass user%40host
'

test_expect_success 'set up repo with http submodules' '
Expand All @@ -142,7 +142,7 @@ test_expect_success 'cmdline credential config passes to submodule via clone' '
set_askpass wrong pass@host &&
git -c "credential.$HTTPD_URL.username=user@host" \
clone --recursive super super-clone &&
expect_askpass pass user@host
expect_askpass pass user%40host
'

test_expect_success 'cmdline credential config passes submodule via fetch' '
Expand All @@ -153,7 +153,7 @@ test_expect_success 'cmdline credential config passes submodule via fetch' '
git -C super-clone \
-c "credential.$HTTPD_URL.username=user@host" \
fetch --recurse-submodules &&
expect_askpass pass user@host
expect_askpass pass user%40host
'

test_expect_success 'cmdline credential config passes submodule update' '
Expand All @@ -170,7 +170,7 @@ test_expect_success 'cmdline credential config passes submodule update' '
git -C super-clone \
-c "credential.$HTTPD_URL.username=user@host" \
submodule update &&
expect_askpass pass user@host
expect_askpass pass user%40host
'

test_expect_success 'fetch changes via http' '
Expand Down
16 changes: 8 additions & 8 deletions t/t5551-http-fetch-smart.sh
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ test_expect_success 'clone from password-protected repository' '
echo two >expect &&
set_askpass user@host pass@host &&
git clone --bare "$HTTPD_URL/auth/smart/repo.git" smart-auth &&
expect_askpass both user@host &&
expect_askpass both user%40host &&
git --git-dir=smart-auth log -1 --format=%s >actual &&
test_cmp expect actual
'
Expand All @@ -199,7 +199,7 @@ test_expect_success 'clone from auth-only-for-objects repository' '
echo two >expect &&
set_askpass user@host pass@host &&
git clone --bare "$HTTPD_URL/auth-fetch/smart/repo.git" half-auth &&
expect_askpass both user@host &&
expect_askpass both user%40host &&
git --git-dir=half-auth log -1 --format=%s >actual &&
test_cmp expect actual
'
Expand All @@ -224,14 +224,14 @@ test_expect_success 'redirects send auth to new location' '
set_askpass user@host pass@host &&
git -c credential.useHttpPath=true \
clone $HTTPD_URL/smart-redir-auth/repo.git repo-redir-auth &&
expect_askpass both user@host auth/smart/repo.git
expect_askpass both user%40host auth/smart/repo.git
'

test_expect_success 'GIT_TRACE_CURL redacts auth details' '
rm -rf redact-auth trace &&
set_askpass user@host pass@host &&
GIT_TRACE_CURL="$(pwd)/trace" git clone --bare "$HTTPD_URL/auth/smart/repo.git" redact-auth &&
expect_askpass both user@host &&
expect_askpass both user%40host &&
# Ensure that there is no "Basic" followed by a base64 string, but that
# the auth details are redacted
Expand All @@ -243,7 +243,7 @@ test_expect_success 'GIT_CURL_VERBOSE redacts auth details' '
rm -rf redact-auth trace &&
set_askpass user@host pass@host &&
GIT_CURL_VERBOSE=1 git clone --bare "$HTTPD_URL/auth/smart/repo.git" redact-auth 2>trace &&
expect_askpass both user@host &&
expect_askpass both user%40host &&
# Ensure that there is no "Basic" followed by a base64 string, but that
# the auth details are redacted
Expand All @@ -256,7 +256,7 @@ test_expect_success 'GIT_TRACE_CURL does not redact auth details if GIT_TRACE_RE
set_askpass user@host pass@host &&
GIT_TRACE_REDACT=0 GIT_TRACE_CURL="$(pwd)/trace" \
git clone --bare "$HTTPD_URL/auth/smart/repo.git" redact-auth &&
expect_askpass both user@host &&
expect_askpass both user%40host &&
grep -i "Authorization: Basic [0-9a-zA-Z+/]" trace
'
Expand Down Expand Up @@ -570,7 +570,7 @@ test_expect_success 'http auth remembers successful credentials' '
# the first request prompts the user...
set_askpass user@host pass@host &&
git ls-remote "$HTTPD_URL/auth/smart/repo.git" >/dev/null &&
expect_askpass both user@host &&
expect_askpass both user%40host &&
# ...and the second one uses the stored value rather than
# prompting the user.
Expand Down Expand Up @@ -601,7 +601,7 @@ test_expect_success 'http auth forgets bogus credentials' '
# us to prompt the user again.
set_askpass user@host pass@host &&
git ls-remote "$HTTPD_URL/auth/smart/repo.git" >/dev/null &&
expect_askpass both user@host
expect_askpass both user%40host
'

test_expect_success 'client falls back from v2 to v0 to match server' '
Expand Down

0 comments on commit 5d50f4e

Please sign in to comment.