]> git.ipfire.org Git - thirdparty/git.git/commitdiff
remote: create fetch.credentialsInUrl config
authorDerrick Stolee <derrickstolee@github.com>
Mon, 6 Jun 2022 14:36:16 +0000 (14:36 +0000)
committerJunio C Hamano <gitster@pobox.com>
Mon, 6 Jun 2022 16:32:32 +0000 (09:32 -0700)
Users sometimes provide a "username:password" combination in their
plaintext URLs. Since Git stores these URLs in plaintext in the
.git/config file, this is a very insecure way of storing these
credentials. Credential managers are a more secure way of storing this
information.

System administrators might want to prevent this kind of use by users on
their machines.

Create a new "fetch.credentialsInUrl" config option and teach Git to
warn or die when seeing a URL with this kind of information. The warning
anonymizes the sensitive information of the URL to be clear about the
issue.

This change currently defaults the behavior to "allow" which does
nothing with these URLs. We can consider changing this behavior to
"warn" by default if we wish. At that time, we may want to add some
advice about setting fetch.credentialsInUrl=ignore for users who still
want to follow this pattern (and not receive the warning).

An earlier version of this change injected the logic into
url_normalize() in urlmatch.c. While most code paths that parse URLs
eventually normalize the URL, that normalization does not happen early
enough in the stack to avoid attempting connections to the URL first. By
inserting a check into the remote validation, we identify the issue
before making a connection. In the old code path, this was revealed by
testing the new t5601-clone.sh test under --stress, resulting in an
instance where the return code was 13 (SIGPIPE) instead of 128 from the
die().

However, we can reuse the parsing information from url_normalize() in
order to benefit from its well-worn parsing logic. We can use the struct
url_info that is created in that method to replace the password with
"<redacted>" in our error messages. This comes with a slight downside
that the normalized URL might look slightly different from the input URL
(for instance, the normalized version adds a closing slash). This should
not hinder users figuring out what the problem is and being able to fix
the issue.

As an attempt to ensure the parsing logic did not catch any
unintentional cases, I modified this change locally to to use the "die"
option by default. Running the test suite succeeds except for the
explicit username:password URLs used in t5550-http-fetch-dumb.sh and
t5541-http-push-smart.sh. This means that all other tested URLs did not
trigger this logic.

The tests show that the proper error messages appear (or do not
appear), but also count the number of error messages. When only warning,
each process validates the remote URL and outputs a warning. This
happens twice for clone, three times for fetch, and once for push.

Helped-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Derrick Stolee <derrickstolee@github.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/config/fetch.txt
remote.c
t/t5516-fetch-push.sh
t/t5601-clone.sh

index 63748c02b72afe5211a710e963c2be502888545e..e425ac012870a074768a17ef59ca29c4da5df8d6 100644 (file)
@@ -95,3 +95,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
+       `<protocol>://<user>:<password>@<domain>/<path>`. 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.
index a6d8ec6c1ac72f8b3b95978d11c87dd0b4918c01..6247fd4faeecf3b8f317fb7b4d77de2723e70c0e 100644 (file)
--- 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"
@@ -613,6 +614,50 @@ 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;
+       int warn_not_die;
+
+       if (git_config_get_string_tmp("fetch.credentialsinurl", &value))
+               return;
+
+       if (!strcmp("warn", value))
+               warn_not_die = 1;
+       else if (!strcmp("die", value))
+               warn_not_die = 0;
+       else if (!strcmp("allow", value))
+               return;
+       else
+               die(_("unrecognized value fetch.credentialsInURL: '%s'"), value);
+
+       for (i = 0; i < remote->url_nr; i++) {
+               struct url_info url_info = { 0 };
+
+               if (!url_normalize(remote->url[i], &url_info) ||
+                   !url_info.passwd_off)
+                       goto loop_cleanup;
+
+               strbuf_reset(&redacted);
+               strbuf_add(&redacted, url_info.url, url_info.passwd_off);
+               strbuf_addstr(&redacted, "<redacted>");
+               strbuf_addstr(&redacted,
+                             url_info.url + url_info.passwd_off + url_info.passwd_len);
+
+               if (warn_not_die)
+                       warning(_("URL '%s' uses plaintext credentials"), redacted.buf);
+               else
+                       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 *,
@@ -638,6 +683,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;
 }
 
index 2f04cf9a1c7700c599237cee93c3877a0f9f649e..a67acc3263949c3de99f7d7c49354f0cda7a623c 100755 (executable)
@@ -12,6 +12,7 @@ This test checks the following functionality:
 * --porcelain output format
 * hiderefs
 * reflogs
+* URL validation
 '
 
 GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
@@ -1809,4 +1810,35 @@ test_expect_success 'refuse fetch to current branch of bare repository worktree'
        git -C bare.git fetch -u .. HEAD:wt
 '
 
+test_expect_success 'fetch warns or fails when using username:password' '
+       message="URL '\''https://username:<redacted>@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_must_fail git -c fetch.credentialsInUrl=die fetch https://username:@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:<redacted>@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
index 83c24fc97a7b1f067f1c04bf93ba849e9ffff945..cf0a3ef3f4c9665e352c2ac3d7033766693b6f7e 100755 (executable)
@@ -71,6 +71,29 @@ test_expect_success 'clone respects GIT_WORK_TREE' '
 
 '
 
+test_expect_success 'clone warns or fails when using username:password' '
+       message="URL '\''https://username:<redacted>@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_must_fail git -c fetch.credentialsInUrl=die clone https://username:@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 &&