diff --git a/Documentation/config/fetch.txt b/Documentation/config/fetch.txt index cd65d236b43ffc..0db7fe85bb8c87 100644 --- a/Documentation/config/fetch.txt +++ b/Documentation/config/fetch.txt @@ -96,3 +96,17 @@ fetch.writeCommitGraph:: merge and the write may take longer. Having an updated commit-graph file helps performance of many Git commands, including `git merge-base`, `git push -f`, and `git log --graph`. Defaults to false. + +fetch.credentialsInUrl:: + A URL can contain plaintext credentials in the form + `://:@/`. Using such URLs + is not recommended as it exposes the password in multiple ways, + including Git storing the URL as plaintext in the repository config. + The `fetch.credentialsInUrl` option provides instruction for how Git + should react to seeing such a URL, with these values: ++ +* `allow` (default): Git will proceed with its activity without warning. +* `warn`: Git will write a warning message to `stderr` when parsing a URL + with a plaintext credential. +* `die`: Git will write a failure message to `stderr` when parsing a URL + with a plaintext credential. diff --git a/remote.c b/remote.c index 930fdc9c2f606a..1f4a95e984012d 100644 --- a/remote.c +++ b/remote.c @@ -1,6 +1,7 @@ #include "cache.h" #include "config.h" #include "remote.h" +#include "urlmatch.h" #include "refs.h" #include "refspec.h" #include "object-store.h" @@ -614,6 +615,39 @@ const char *remote_ref_for_branch(struct branch *branch, int for_push) return NULL; } +static void validate_remote_url(struct remote *remote) +{ + int i; + const char *value; + struct strbuf redacted = STRBUF_INIT; + + if (git_config_get_string_tmp("fetch.credentialsinurl", &value) || + !strcmp("allow", value)) + return; + + for (i = 0; i < remote->url_nr; i++) { + struct url_info url_info = { NULL }; + url_normalize(remote->url[i], &url_info); + + if (!url_info.passwd_len) + goto loop_cleanup; + + strbuf_add(&redacted, url_info.url, url_info.passwd_off); + strbuf_addstr(&redacted, ""); + strbuf_addstr(&redacted, url_info.url + url_info.passwd_off + url_info.passwd_len); + + if (!strcmp("warn", value)) + warning(_("URL '%s' uses plaintext credentials"), redacted.buf); + if (!strcmp("die", value)) + die(_("URL '%s' uses plaintext credentials"), redacted.buf); + +loop_cleanup: + free(url_info.url); + } + + strbuf_release(&redacted); +} + static struct remote * remotes_remote_get_1(struct remote_state *remote_state, const char *name, const char *(*get_default)(struct remote_state *, @@ -639,6 +673,9 @@ remotes_remote_get_1(struct remote_state *remote_state, const char *name, add_url_alias(remote_state, ret, name); if (!valid_remote(ret)) return NULL; + + validate_remote_url(ret); + return ret; } diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index 4dfb080433e0a0..4497617db7f312 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -12,6 +12,7 @@ This test checks the following functionality: * --porcelain output format * hiderefs * reflogs +* URL validation ' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main @@ -1813,4 +1814,31 @@ test_expect_success 'refuse to push a hidden ref, and make sure do not pollute t test_dir_is_empty testrepo/.git/objects/pack ' +test_expect_success 'fetch warns or fails when using username:password' ' + message="URL '\''https://username:@localhost/'\'' uses plaintext credentials" && + test_must_fail git -c fetch.credentialsInUrl=allow fetch https://username:password@localhost 2>err && + ! grep "$message" err && + + test_must_fail git -c fetch.credentialsInUrl=warn fetch https://username:password@localhost 2>err && + grep "warning: $message" err >warnings && + test_line_count = 3 warnings && + + test_must_fail git -c fetch.credentialsInUrl=die fetch https://username:password@localhost 2>err && + grep "fatal: $message" err >warnings && + test_line_count = 1 warnings +' + + +test_expect_success 'push warns or fails when using username:password' ' + message="URL '\''https://username:@localhost/'\'' uses plaintext credentials" && + test_must_fail git -c fetch.credentialsInUrl=allow push https://username:password@localhost 2>err && + ! grep "$message" err && + + test_must_fail git -c fetch.credentialsInUrl=warn push https://username:password@localhost 2>err && + grep "warning: $message" err >warnings && + test_must_fail git -c fetch.credentialsInUrl=die push https://username:password@localhost 2>err && + grep "fatal: $message" err >warnings && + test_line_count = 1 warnings +' + test_done diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh index 4a61f2c901ea3d..d03c19f5e07ead 100755 --- a/t/t5601-clone.sh +++ b/t/t5601-clone.sh @@ -71,6 +71,25 @@ test_expect_success 'clone respects GIT_WORK_TREE' ' ' +test_expect_success 'clone warns or fails when using username:password' ' + message="URL '\''https://username:@localhost/'\'' uses plaintext credentials" && + test_must_fail git -c fetch.credentialsInUrl=allow clone https://username:password@localhost attempt1 2>err && + ! grep "$message" err && + + test_must_fail git -c fetch.credentialsInUrl=warn clone https://username:password@localhost attempt2 2>err && + grep "warning: $message" err >warnings && + test_line_count = 2 warnings && + + test_must_fail git -c fetch.credentialsInUrl=die clone https://username:password@localhost attempt3 2>err && + grep "fatal: $message" err >warnings && + test_line_count = 1 warnings +' + +test_expect_success 'clone does not detect username:password when it is https://username@domain:port/' ' + test_must_fail git -c fetch.credentialsInUrl=warn clone https://username@localhost:8080 attempt3 2>err && + ! grep "uses plaintext credentials" err +' + test_expect_success 'clone from hooks' ' test_create_repo r0 &&