Brad Bishop | 220d553 | 2018-08-14 00:59:39 +0100 | [diff] [blame] | 1 | From 0383bbb9015898cbc79abd7b64316484d7713b44 Mon Sep 17 00:00:00 2001 |
| 2 | From: Jeff King <peff@peff.net> |
| 3 | Date: Mon, 30 Apr 2018 03:25:25 -0400 |
| 4 | Subject: [PATCH] submodule-config: verify submodule names as paths |
| 5 | |
| 6 | Submodule "names" come from the untrusted .gitmodules file, |
| 7 | but we blindly append them to $GIT_DIR/modules to create our |
| 8 | on-disk repo paths. This means you can do bad things by |
| 9 | putting "../" into the name (among other things). |
| 10 | |
| 11 | Let's sanity-check these names to avoid building a path that |
| 12 | can be exploited. There are two main decisions: |
| 13 | |
| 14 | 1. What should the allowed syntax be? |
| 15 | |
| 16 | It's tempting to reuse verify_path(), since submodule |
| 17 | names typically come from in-repo paths. But there are |
| 18 | two reasons not to: |
| 19 | |
| 20 | a. It's technically more strict than what we need, as |
| 21 | we really care only about breaking out of the |
| 22 | $GIT_DIR/modules/ hierarchy. E.g., having a |
| 23 | submodule named "foo/.git" isn't actually |
| 24 | dangerous, and it's possible that somebody has |
| 25 | manually given such a funny name. |
| 26 | |
| 27 | b. Since we'll eventually use this checking logic in |
| 28 | fsck to prevent downstream repositories, it should |
| 29 | be consistent across platforms. Because |
| 30 | verify_path() relies on is_dir_sep(), it wouldn't |
| 31 | block "foo\..\bar" on a non-Windows machine. |
| 32 | |
| 33 | 2. Where should we enforce it? These days most of the |
| 34 | .gitmodules reads go through submodule-config.c, so |
| 35 | I've put it there in the reading step. That should |
| 36 | cover all of the C code. |
| 37 | |
| 38 | We also construct the name for "git submodule add" |
| 39 | inside the git-submodule.sh script. This is probably |
| 40 | not a big deal for security since the name is coming |
| 41 | from the user anyway, but it would be polite to remind |
| 42 | them if the name they pick is invalid (and we need to |
| 43 | expose the name-checker to the shell anyway for our |
| 44 | test scripts). |
| 45 | |
| 46 | This patch issues a warning when reading .gitmodules |
| 47 | and just ignores the related config entry completely. |
| 48 | This will generally end up producing a sensible error, |
| 49 | as it works the same as a .gitmodules file which is |
| 50 | missing a submodule entry (so "submodule update" will |
| 51 | barf, but "git clone --recurse-submodules" will print |
| 52 | an error but not abort the clone. |
| 53 | |
| 54 | There is one minor oddity, which is that we print the |
| 55 | warning once per malformed config key (since that's how |
| 56 | the config subsystem gives us the entries). So in the |
| 57 | new test, for example, the user would see three |
| 58 | warnings. That's OK, since the intent is that this case |
| 59 | should never come up outside of malicious repositories |
| 60 | (and then it might even benefit the user to see the |
| 61 | message multiple times). |
| 62 | |
| 63 | Credit for finding this vulnerability and the proof of |
| 64 | concept from which the test script was adapted goes to |
| 65 | Etienne Stalmans. |
| 66 | |
| 67 | CVE: CVE-2018-11235 |
| 68 | Upstream-Status: Backport [https://github.com/gitster/git/commit/0383bbb9015898cbc79abd7b64316484d7713b44#diff-1772b951776d1647ca31a2256f7fe88f] |
| 69 | |
| 70 | Signed-off-by: Jeff King <peff@peff.net> |
| 71 | Signed-off-by: Jagadeesh Krishnanjanappa <jkrishnanjanappa@mvista.com> |
| 72 | --- |
| 73 | builtin/submodule--helper.c | 24 ++++++++++++++ |
| 74 | git-submodule.sh | 5 +++ |
| 75 | submodule-config.c | 31 ++++++++++++++++++ |
| 76 | submodule-config.h | 7 +++++ |
| 77 | t/t7415-submodule-names.sh | 76 +++++++++++++++++++++++++++++++++++++++++++++ |
| 78 | 5 files changed, 143 insertions(+) |
| 79 | create mode 100755 t/t7415-submodule-names.sh |
| 80 | |
| 81 | diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c |
| 82 | index cbb17a902..b4b4d29d8 100644 |
| 83 | --- a/builtin/submodule--helper.c |
| 84 | +++ b/builtin/submodule--helper.c |
| 85 | @@ -1480,6 +1480,29 @@ static int is_active(int argc, const cha |
| 86 | return !is_submodule_active(the_repository, argv[1]); |
| 87 | } |
| 88 | |
| 89 | +/* |
| 90 | + * Exit non-zero if any of the submodule names given on the command line is |
| 91 | + * invalid. If no names are given, filter stdin to print only valid names |
| 92 | + * (which is primarily intended for testing). |
| 93 | + */ |
| 94 | +static int check_name(int argc, const char **argv, const char *prefix) |
| 95 | +{ |
| 96 | + if (argc > 1) { |
| 97 | + while (*++argv) { |
| 98 | + if (check_submodule_name(*argv) < 0) |
| 99 | + return 1; |
| 100 | + } |
| 101 | + } else { |
| 102 | + struct strbuf buf = STRBUF_INIT; |
| 103 | + while (strbuf_getline(&buf, stdin) != EOF) { |
| 104 | + if (!check_submodule_name(buf.buf)) |
| 105 | + printf("%s\n", buf.buf); |
| 106 | + } |
| 107 | + strbuf_release(&buf); |
| 108 | + } |
| 109 | + return 0; |
| 110 | +} |
| 111 | + |
| 112 | #define SUPPORT_SUPER_PREFIX (1<<0) |
| 113 | |
| 114 | struct cmd_struct { |
| 115 | @@ -1502,6 +1525,7 @@ static struct cmd_struct commands[] = { |
| 116 | {"push-check", push_check, 0}, |
| 117 | {"absorb-git-dirs", absorb_git_dirs, SUPPORT_SUPER_PREFIX}, |
| 118 | {"is-active", is_active, 0}, |
| 119 | + {"check-name", check_name, 0}, |
| 120 | }; |
| 121 | |
| 122 | int cmd_submodule__helper(int argc, const char **argv, const char *prefix) |
| 123 | diff --git a/git-submodule.sh b/git-submodule.sh |
| 124 | index c0d0e9a4c..92750b9e2 100755 |
| 125 | --- a/git-submodule.sh |
| 126 | +++ b/git-submodule.sh |
| 127 | @@ -229,6 +229,11 @@ Use -f if you really want to add it." >& |
| 128 | sm_name="$sm_path" |
| 129 | fi |
| 130 | |
| 131 | + if ! git submodule--helper check-name "$sm_name" |
| 132 | + then |
| 133 | + die "$(eval_gettext "'$sm_name' is not a valid submodule name")" |
| 134 | + fi |
| 135 | + |
| 136 | # perhaps the path exists and is already a git repo, else clone it |
| 137 | if test -e "$sm_path" |
| 138 | then |
| 139 | diff --git a/submodule-config.c b/submodule-config.c |
| 140 | index 4f58491dd..de54351c6 100644 |
| 141 | --- a/submodule-config.c |
| 142 | +++ b/submodule-config.c |
| 143 | @@ -190,6 +190,31 @@ static struct submodule *cache_lookup_na |
| 144 | return NULL; |
| 145 | } |
| 146 | |
| 147 | +int check_submodule_name(const char *name) |
| 148 | +{ |
| 149 | + /* Disallow empty names */ |
| 150 | + if (!*name) |
| 151 | + return -1; |
| 152 | + |
| 153 | + /* |
| 154 | + * Look for '..' as a path component. Check both '/' and '\\' as |
| 155 | + * separators rather than is_dir_sep(), because we want the name rules |
| 156 | + * to be consistent across platforms. |
| 157 | + */ |
| 158 | + goto in_component; /* always start inside component */ |
| 159 | + while (*name) { |
| 160 | + char c = *name++; |
| 161 | + if (c == '/' || c == '\\') { |
| 162 | +in_component: |
| 163 | + if (name[0] == '.' && name[1] == '.' && |
| 164 | + (!name[2] || name[2] == '/' || name[2] == '\\')) |
| 165 | + return -1; |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + return 0; |
| 170 | +} |
| 171 | + |
| 172 | static int name_and_item_from_var(const char *var, struct strbuf *name, |
| 173 | struct strbuf *item) |
| 174 | { |
| 175 | @@ -201,6 +226,12 @@ static int name_and_item_from_var(const |
| 176 | return 0; |
| 177 | |
| 178 | strbuf_add(name, subsection, subsection_len); |
| 179 | + if (check_submodule_name(name->buf) < 0) { |
| 180 | + warning(_("ignoring suspicious submodule name: %s"), name->buf); |
| 181 | + strbuf_release(name); |
| 182 | + return 0; |
| 183 | + } |
| 184 | + |
| 185 | strbuf_addstr(item, key); |
| 186 | |
| 187 | return 1; |
| 188 | diff --git a/submodule-config.h b/submodule-config.h |
| 189 | index d434ecdb4..103cc79dd 100644 |
| 190 | --- a/submodule-config.h |
| 191 | +++ b/submodule-config.h |
| 192 | @@ -48,4 +48,11 @@ extern const struct submodule *submodule |
| 193 | const char *key); |
| 194 | extern void submodule_free(void); |
| 195 | |
| 196 | +/* |
| 197 | + * Returns 0 if the name is syntactically acceptable as a submodule "name" |
| 198 | + * (e.g., that may be found in the subsection of a .gitmodules file) and -1 |
| 199 | + * otherwise. |
| 200 | + */ |
| 201 | +int check_submodule_name(const char *name); |
| 202 | + |
| 203 | #endif /* SUBMODULE_CONFIG_H */ |
| 204 | diff --git a/t/t7415-submodule-names.sh b/t/t7415-submodule-names.sh |
| 205 | new file mode 100755 |
| 206 | index 000000000..75fa071c6 |
| 207 | --- /dev/null |
| 208 | +++ b/t/t7415-submodule-names.sh |
| 209 | @@ -0,0 +1,76 @@ |
| 210 | +#!/bin/sh |
| 211 | + |
| 212 | +test_description='check handling of .. in submodule names |
| 213 | + |
| 214 | +Exercise the name-checking function on a variety of names, and then give a |
| 215 | +real-world setup that confirms we catch this in practice. |
| 216 | +' |
| 217 | +. ./test-lib.sh |
| 218 | + |
| 219 | +test_expect_success 'check names' ' |
| 220 | + cat >expect <<-\EOF && |
| 221 | + valid |
| 222 | + valid/with/paths |
| 223 | + EOF |
| 224 | + |
| 225 | + git submodule--helper check-name >actual <<-\EOF && |
| 226 | + valid |
| 227 | + valid/with/paths |
| 228 | + |
| 229 | + ../foo |
| 230 | + /../foo |
| 231 | + ..\foo |
| 232 | + \..\foo |
| 233 | + foo/.. |
| 234 | + foo/../ |
| 235 | + foo\.. |
| 236 | + foo\..\ |
| 237 | + foo/../bar |
| 238 | + EOF |
| 239 | + |
| 240 | + test_cmp expect actual |
| 241 | +' |
| 242 | + |
| 243 | +test_expect_success 'create innocent subrepo' ' |
| 244 | + git init innocent && |
| 245 | + git -C innocent commit --allow-empty -m foo |
| 246 | +' |
| 247 | + |
| 248 | +test_expect_success 'submodule add refuses invalid names' ' |
| 249 | + test_must_fail \ |
| 250 | + git submodule add --name ../../modules/evil "$PWD/innocent" evil |
| 251 | +' |
| 252 | + |
| 253 | +test_expect_success 'add evil submodule' ' |
| 254 | + git submodule add "$PWD/innocent" evil && |
| 255 | + |
| 256 | + mkdir modules && |
| 257 | + cp -r .git/modules/evil modules && |
| 258 | + write_script modules/evil/hooks/post-checkout <<-\EOF && |
| 259 | + echo >&2 "RUNNING POST CHECKOUT" |
| 260 | + EOF |
| 261 | + |
| 262 | + git config -f .gitmodules submodule.evil.update checkout && |
| 263 | + git config -f .gitmodules --rename-section \ |
| 264 | + submodule.evil submodule.../../modules/evil && |
| 265 | + git add modules && |
| 266 | + git commit -am evil |
| 267 | +' |
| 268 | + |
| 269 | +# This step seems like it shouldn't be necessary, since the payload is |
| 270 | +# contained entirely in the evil submodule. But due to the vagaries of the |
| 271 | +# submodule code, checking out the evil module will fail unless ".git/modules" |
| 272 | +# exists. Adding another submodule (with a name that sorts before "evil") is an |
| 273 | +# easy way to make sure this is the case in the victim clone. |
| 274 | +test_expect_success 'add other submodule' ' |
| 275 | + git submodule add "$PWD/innocent" another-module && |
| 276 | + git add another-module && |
| 277 | + git commit -am another |
| 278 | +' |
| 279 | + |
| 280 | +test_expect_success 'clone evil superproject' ' |
| 281 | + git clone --recurse-submodules . victim >output 2>&1 && |
| 282 | + ! grep "RUNNING POST CHECKOUT" output |
| 283 | +' |
| 284 | + |
| 285 | +test_done |
| 286 | -- |
| 287 | 2.13.3 |
| 288 | |