transport: add protocol policy config option
authorBrandon Williams <bmwill@google.com>
Wed, 14 Dec 2016 22:39:52 +0000 (14:39 -0800)
committerJunio C Hamano <gitster@pobox.com>
Thu, 15 Dec 2016 17:29:13 +0000 (09:29 -0800)
Previously the `GIT_ALLOW_PROTOCOL` environment variable was used to
specify a whitelist of protocols to be used in clone/fetch/push
commands. This patch introduces new configuration options for more
fine-grained control for allowing/disallowing protocols. This also has
the added benefit of allowing easier construction of a protocol
whitelist on systems where setting an environment variable is
non-trivial.

Now users can specify a policy to be used for each type of protocol via
the 'protocol.<name>.allow' config option. A default policy for all
unconfigured protocols can be set with the 'protocol.allow' config
option. If no user configured default is made git will allow known-safe
protocols (http, https, git, ssh, file), disallow known-dangerous
protocols (ext), and have a default policy of `user` for all other
protocols.

The supported policies are `always`, `never`, and `user`. The `user`
policy can be used to configure a protocol to be usable when explicitly
used by a user, while disallowing it for commands which run
clone/fetch/push commands without direct user intervention (e.g.
recursive initialization of submodules). Commands which can potentially
clone/fetch/push from untrusted repositories without user intervention
can export `GIT_PROTOCOL_FROM_USER` with a value of '0' to prevent
protocols configured to the `user` policy from being used.

Fix remote-ext tests to use the new config to allow the ext
protocol to be tested.

Based on a patch by Jeff King <peff@peff.net>

Signed-off-by: Brandon Williams <bmwill@google.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/config.txt
Documentation/git.txt
git-submodule.sh
t/lib-proto-disable.sh
t/t5509-fetch-push-namespaces.sh
t/t5802-connect-helper.sh
transport.c
index 81533364352ec51c55291138987506ce41cc9ef4..50d3d06ffa0a279649b26f15a5aa5768f49d57d3 100644 (file)
@@ -2260,6 +2260,52 @@ pretty.<name>::
        Note that an alias with the same name as a built-in format
        will be silently ignored.
 
+protocol.allow::
+       If set, provide a user defined default policy for all protocols which
+       don't explicitly have a policy (`protocol.<name>.allow`).  By default,
+       if unset, known-safe protocols (http, https, git, ssh, file) have a
+       default policy of `always`, known-dangerous protocols (ext) have a
+       default policy of `never`, and all other protocols have a default
+       policy of `user`.  Supported policies:
++
+--
+
+* `always` - protocol is always able to be used.
+
+* `never` - protocol is never able to be used.
+
+* `user` - protocol is only able to be used when `GIT_PROTOCOL_FROM_USER` is
+  either unset or has a value of 1.  This policy should be used when you want a
+  protocol to be directly usable by the user but don't want it used by commands which
+  execute clone/fetch/push commands without user input, e.g. recursive
+  submodule initialization.
+
+--
+
+protocol.<name>.allow::
+       Set a policy to be used by protocol `<name>` with clone/fetch/push
+       commands. See `protocol.allow` above for the available policies.
++
+The protocol names currently used by git are:
++
+--
+  - `file`: any local file-based path (including `file://` URLs,
+    or local paths)
+
+  - `git`: the anonymous git protocol over a direct TCP
+    connection (or proxy, if configured)
+
+  - `ssh`: git over ssh (including `host:path` syntax,
+    `ssh://`, etc).
+
+  - `http`: git over http, both "smart http" and "dumb http".
+    Note that this does _not_ include `https`; if you want to configure
+    both, you must do so individually.
+
+  - any external helpers are named by their protocol (e.g., use
+    `hg` to allow the `git-remote-hg` helper)
+--
+
 pull.ff::
        By default, Git does not create an extra merge commit when merging
        a commit that is a descendant of the current commit. Instead, the
index 923aa49db7f67ca6cb4510852f941654c55c59b2..d9fb937586c8dfe58348aa4d3cdb8b23d7b7fc12 100644 (file)
@@ -1129,30 +1129,20 @@ of clones and fetches.
        cloning a repository to make a backup).
 
 `GIT_ALLOW_PROTOCOL`::
-       If set, provide a colon-separated list of protocols which are
-       allowed to be used with fetch/push/clone. This is useful to
-       restrict recursive submodule initialization from an untrusted
-       repository. Any protocol not mentioned will be disallowed (i.e.,
-       this is a whitelist, not a blacklist). If the variable is not
-       set at all, all protocols are enabled.  The protocol names
-       currently used by git are:
-
-         - `file`: any local file-based path (including `file://` URLs,
-           or local paths)
-
-         - `git`: the anonymous git protocol over a direct TCP
-           connection (or proxy, if configured)
-
-         - `ssh`: git over ssh (including `host:path` syntax,
-           `ssh://`, etc).
-
-         - `http`: git over http, both "smart http" and "dumb http".
-           Note that this does _not_ include `https`; if you want both,
-           you should specify both as `http:https`.
-
-         - any external helpers are named by their protocol (e.g., use
-           `hg` to allow the `git-remote-hg` helper)
-
+       If set to a colon-separated list of protocols, behave as if
+       `protocol.allow` is set to `never`, and each of the listed
+       protocols has `protocol.<name>.allow` set to `always`
+       (overriding any existing configuration). In other words, any
+       protocol not mentioned will be disallowed (i.e., this is a
+       whitelist, not a blacklist). See the description of
+       `protocol.allow` in linkgit:git-config[1] for more details.
+
+`GIT_PROTOCOL_FROM_USER`::
+       Set to 0 to prevent protocols used by fetch/push/clone which are
+       configured to the `user` state.  This is useful to restrict recursive
+       submodule initialization from an untrusted repository or for programs
+       which feed potentially-untrusted URLS to git commands.  See
+       linkgit:git-config[1] for more details.
 
 Discussion[[Discussion]]
 ------------------------
index 78fdac95684b2214f61cb30ea04bb5a13691430b..fc440761af944bdbb8c6a768aec2fe9cdfffc3a4 100755 (executable)
@@ -22,14 +22,10 @@ require_work_tree
 wt_prefix=$(git rev-parse --show-prefix)
 cd_to_toplevel
 
-# Restrict ourselves to a vanilla subset of protocols; the URLs
-# we get are under control of a remote repository, and we do not
-# want them kicking off arbitrary git-remote-* programs.
-#
-# If the user has already specified a set of allowed protocols,
-# we assume they know what they're doing and use that instead.
-: ${GIT_ALLOW_PROTOCOL=file:git:http:https:ssh}
-export GIT_ALLOW_PROTOCOL
+# Tell the rest of git that any URLs we get don't come
+# directly from the user, so it can apply policy as appropriate.
+GIT_PROTOCOL_FROM_USER=0
+export GIT_PROTOCOL_FROM_USER
 
 command=
 branch=
index be88e9a00f506c9bb57f3c6eda9e0b5b920ea930..02f49cb4097153f84d5e397b1801128e229c7909 100644 (file)
@@ -1,10 +1,7 @@
 # Test routines for checking protocol disabling.
 
-# test cloning a particular protocol
-#   $1 - description of the protocol
-#   $2 - machine-readable name of the protocol
-#   $3 - the URL to try cloning
-test_proto () {
+# Test clone/fetch/push with GIT_ALLOW_PROTOCOL whitelist
+test_whitelist () {
        desc=$1
        proto=$2
        url=$3
@@ -62,6 +59,129 @@ test_proto () {
                        test_must_fail git clone --bare "$url" tmp.git
                )
        '
+
+       test_expect_success "clone $desc (env var has precedence)" '
+               rm -rf tmp.git &&
+               (
+                       GIT_ALLOW_PROTOCOL=none &&
+                       export GIT_ALLOW_PROTOCOL &&
+                       test_must_fail git -c protocol.allow=always clone --bare "$url" tmp.git &&
+                       test_must_fail git -c protocol.$proto.allow=always clone --bare "$url" tmp.git
+               )
+       '
+}
+
+test_config () {
+       desc=$1
+       proto=$2
+       url=$3
+
+       # Test clone/fetch/push with protocol.<type>.allow config
+       test_expect_success "clone $desc (enabled with config)" '
+               rm -rf tmp.git &&
+               git -c protocol.$proto.allow=always clone --bare "$url" tmp.git
+       '
+
+       test_expect_success "fetch $desc (enabled)" '
+               git -C tmp.git -c protocol.$proto.allow=always fetch
+       '
+
+       test_expect_success "push $desc (enabled)" '
+               git -C tmp.git -c protocol.$proto.allow=always  push origin HEAD:pushed
+       '
+
+       test_expect_success "push $desc (disabled)" '
+               test_must_fail git -C tmp.git -c protocol.$proto.allow=never push origin HEAD:pushed
+       '
+
+       test_expect_success "fetch $desc (disabled)" '
+               test_must_fail git -C tmp.git -c protocol.$proto.allow=never fetch
+       '
+
+       test_expect_success "clone $desc (disabled)" '
+               rm -rf tmp.git &&
+               test_must_fail git -c protocol.$proto.allow=never clone --bare "$url" tmp.git
+       '
+
+       # Test clone/fetch/push with protocol.user.allow and its env var
+       test_expect_success "clone $desc (enabled)" '
+               rm -rf tmp.git &&
+               git -c protocol.$proto.allow=user clone --bare "$url" tmp.git
+       '
+
+       test_expect_success "fetch $desc (enabled)" '
+               git -C tmp.git -c protocol.$proto.allow=user fetch
+       '
+
+       test_expect_success "push $desc (enabled)" '
+               git -C tmp.git -c protocol.$proto.allow=user push origin HEAD:pushed
+       '
+
+       test_expect_success "push $desc (disabled)" '
+               (
+                       cd tmp.git &&
+                       GIT_PROTOCOL_FROM_USER=0 &&
+                       export GIT_PROTOCOL_FROM_USER &&
+                       test_must_fail git -c protocol.$proto.allow=user push origin HEAD:pushed
+               )
+       '
+
+       test_expect_success "fetch $desc (disabled)" '
+               (
+                       cd tmp.git &&
+                       GIT_PROTOCOL_FROM_USER=0 &&
+                       export GIT_PROTOCOL_FROM_USER &&
+                       test_must_fail git -c protocol.$proto.allow=user fetch
+               )
+       '
+
+       test_expect_success "clone $desc (disabled)" '
+               rm -rf tmp.git &&
+               (
+                       GIT_PROTOCOL_FROM_USER=0 &&
+                       export GIT_PROTOCOL_FROM_USER &&
+                       test_must_fail git -c protocol.$proto.allow=user clone --bare "$url" tmp.git
+               )
+       '
+
+       # Test clone/fetch/push with protocol.allow user defined default
+       test_expect_success "clone $desc (enabled)" '
+               rm -rf tmp.git &&
+               git config --global protocol.allow always &&
+               git clone --bare "$url" tmp.git
+       '
+
+       test_expect_success "fetch $desc (enabled)" '
+               git -C tmp.git fetch
+       '
+
+       test_expect_success "push $desc (enabled)" '
+               git -C tmp.git push origin HEAD:pushed
+       '
+
+       test_expect_success "push $desc (disabled)" '
+               git config --global protocol.allow never &&
+               test_must_fail git -C tmp.git push origin HEAD:pushed
+       '
+
+       test_expect_success "fetch $desc (disabled)" '
+               test_must_fail git -C tmp.git fetch
+       '
+
+       test_expect_success "clone $desc (disabled)" '
+               rm -rf tmp.git &&
+               test_must_fail git clone --bare "$url" tmp.git
+       '
+}
+
+# test cloning a particular protocol
+#   $1 - description of the protocol
+#   $2 - machine-readable name of the protocol
+#   $3 - the URL to try cloning
+test_proto () {
+       test_whitelist "$@"
+
+       test_config "$@"
 }
 
 # set up an ssh wrapper that will access $host/$repo in the
index bc44ac36d57615b516018a2602c15bea8085e316..75c570adcae4f474c15775ec8504521fc806e2a9 100755 (executable)
@@ -4,6 +4,7 @@ test_description='fetch/push involving ref namespaces'
 . ./test-lib.sh
 
 test_expect_success setup '
+       git config --global protocol.ext.allow user &&
        test_tick &&
        git init original &&
        (
index b7a7f9d5886f1e2f3fc2d65bfcf1785b890638ac..c6c2661878c0ca8a22ad11e1229a664f3d009fee 100755 (executable)
@@ -4,6 +4,7 @@ test_description='ext::cmd remote "connect" helper'
 . ./test-lib.sh
 
 test_expect_success setup '
+       git config --global protocol.ext.allow user &&
        test_tick &&
        git commit --allow-empty -m initial &&
        test_tick &&
index dff929ec01fdb8a51e74cd7c2036fd998208b513..fbd799d062577d87f9ffdd8a9e7dcec306a299c2 100644 (file)
@@ -617,10 +617,81 @@ static const struct string_list *protocol_whitelist(void)
        return enabled ? &allowed : NULL;
 }
 
+enum protocol_allow_config {
+       PROTOCOL_ALLOW_NEVER = 0,
+       PROTOCOL_ALLOW_USER_ONLY,
+       PROTOCOL_ALLOW_ALWAYS
+};
+
+static enum protocol_allow_config parse_protocol_config(const char *key,
+                                                       const char *value)
+{
+       if (!strcasecmp(value, "always"))
+               return PROTOCOL_ALLOW_ALWAYS;
+       else if (!strcasecmp(value, "never"))
+               return PROTOCOL_ALLOW_NEVER;
+       else if (!strcasecmp(value, "user"))
+               return PROTOCOL_ALLOW_USER_ONLY;
+
+       die("unknown value for config '%s': %s", key, value);
+}
+
+static enum protocol_allow_config get_protocol_config(const char *type)
+{
+       char *key = xstrfmt("protocol.%s.allow", type);
+       char *value;
+
+       /* first check the per-protocol config */
+       if (!git_config_get_string(key, &value)) {
+               enum protocol_allow_config ret =
+                       parse_protocol_config(key, value);
+               free(key);
+               free(value);
+               return ret;
+       }
+       free(key);
+
+       /* if defined, fallback to user-defined default for unknown protocols */
+       if (!git_config_get_string("protocol.allow", &value)) {
+               enum protocol_allow_config ret =
+                       parse_protocol_config("protocol.allow", value);
+               free(value);
+               return ret;
+       }
+
+       /* fallback to built-in defaults */
+       /* known safe */
+       if (!strcmp(type, "http") ||
+           !strcmp(type, "https") ||
+           !strcmp(type, "git") ||
+           !strcmp(type, "ssh") ||
+           !strcmp(type, "file"))
+               return PROTOCOL_ALLOW_ALWAYS;
+
+       /* known scary; err on the side of caution */
+       if (!strcmp(type, "ext"))
+               return PROTOCOL_ALLOW_NEVER;
+
+       /* unknown; by default let them be used only directly by the user */
+       return PROTOCOL_ALLOW_USER_ONLY;
+}
+
 int is_transport_allowed(const char *type)
 {
-       const struct string_list *allowed = protocol_whitelist();
-       return !allowed || string_list_has_string(allowed, type);
+       const struct string_list *whitelist = protocol_whitelist();
+       if (whitelist)
+               return string_list_has_string(whitelist, type);
+
+       switch (get_protocol_config(type)) {
+       case PROTOCOL_ALLOW_ALWAYS:
+               return 1;
+       case PROTOCOL_ALLOW_NEVER:
+               return 0;
+       case PROTOCOL_ALLOW_USER_ONLY:
+               return git_env_bool("GIT_PROTOCOL_FROM_USER", 1);
+       }
+
+       die("BUG: invalid protocol_allow_config type");
 }
 
 void transport_check_allowed(const char *type)