Commit message: submodule-config: ban submodule urls that start with dash

The previous commit taught the submodule code to invoke our
"git clone $url $path" with a "--" separator so that we
aren't confused by urls or paths that start with dashes.

However, that's just one code path. It's not clear if there
are others, and it would be an easy mistake to add one in
the future. Moreover, even with the fix in the previous
commit, it's quite hard to actually do anything useful with
such an entry. Any url starting with a dash must fall into
one of three categories:

 - it's meant as a file url, like "-path". But then any
   clone is not going to have the matching path, since it's
   by definition relative inside the newly created clone. If
   you spell it as "./-path", the submodule code sees the
   "/" and translates this to an absolute path, so it at
   least works (assuming the receiver has the same
   filesystem layout as you). But that trick does not apply
   for a bare "-path".

 - it's meant as an ssh url, like "-host:path". But this
   already doesn't work, as we explicitly disallow ssh
   hostnames that begin with a dash (to avoid option
   injection against ssh).

 - it's a remote-helper scheme, like "-scheme::data". This
   _could_ work if the receiver bends over backwards and
   creates a funny-named helper like "git-remote--scheme".
   But normally there would not be any helper that matches.

Since such a url does not work today and is not likely to do
anything useful in the future, let's simply disallow them
entirely. That protects the existing "git clone" path (in a
belt-and-suspenders way), along with any others that might
exist.

Our tests cover two cases:

  1. A file url with "./" continues to work, showing that
     there's an escape hatch for people with truly silly
     repo names.

  2. A url starting with "-" is rejected.

Note that we expect case (2) to fail, but it would have done
so even without this commit, for the reasons given above.
So instead of just expecting failure, let's also check for
the magic word "ignoring" on stderr. That lets us know that
we failed for the right reason.

Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
CVE: CVE-2018-17456
CWE: CWE-88
Vulnerability description: Git before 2.14.5, 2.15.x before 2.15.3, 2.16.x before 2.16.5, 2.17.x before 2.17.2, 2.18.x before 2.18.1, and 2.19.x before 2.19.1 allows remote code execution during processing of a recursive "git clone" of a superproject if a .gitmodules file has a URL field beginning with a '-' character.

Project: git
File: submodule-config.c

int submodule_config_option(struct repository *repo,
			    const char *var, const char *value)
{
	struct parse_config_parameter parameter;

	submodule_cache_check_init(repo);

	parameter.cache = repo->submodule_cache;
	parameter.treeish_name = NULL;
	parameter.gitmodules_sha1 = null_sha1;
	parameter.overwrite = 1;

	return parse_config(var, value, &parameter);
}

Project: git
File: submodule-config.c

static void submodule_cache_clear(struct submodule_cache *cache)
{
	struct hashmap_iter iter;
	struct submodule_entry *entry;

	if (!cache->initialized)
		return;

	/*
	 * We iterate over the name hash here to be symmetric with the
	 * allocation of struct submodule entries. Each is allocated by
	 * their .gitmodule blob sha1 and submodule name.
	 */
	hashmap_iter_init(&cache->for_name, &iter);
	while ((entry = hashmap_iter_next(&iter)))
		free_one_config(entry);

	hashmap_free(&cache->for_path, 1);
	hashmap_free(&cache->for_name, 1);
	cache->initialized = 0;
}

Project: git
File: submodule-config.c

int gitmodule_sha1_from_commit(const unsigned char *treeish_name,
				      unsigned char *gitmodules_sha1,
				      struct strbuf *rev)
{
	int ret = 0;

	if (is_null_sha1(treeish_name)) {
		hashclr(gitmodules_sha1);
		return 1;
	}

	strbuf_addf(rev, "%s:.gitmodules", sha1_to_hex(treeish_name));
	if (get_sha1(rev->buf, gitmodules_sha1) >= 0)
		ret = 1;

	return ret;
}

Project: git
File: submodule-config.c

static int parse_update_recurse(const char *opt, const char *arg,
				int die_on_error)
{
	switch (git_config_maybe_bool(opt, arg)) {
	case 1:
		return RECURSE_SUBMODULES_ON;
	case 0:
		return RECURSE_SUBMODULES_OFF;
	default:
		if (die_on_error)
			die("bad %s argument: %s", opt, arg);
		return RECURSE_SUBMODULES_ERROR;
	}
}

Project: git
File: submodule-config.c

int parse_submodule_config_option(const char *var, const char *value)
{
	return submodule_config_option(the_repository, var, value);
}

Project: git
File: submodule-config.c

static int config_path_cmp(const void *unused_cmp_data,
			   const struct submodule_entry *a,
			   const struct submodule_entry *b,
			   const void *unused_keydata)
{
	return strcmp(a->config->path, b->config->path) ||
	       hashcmp(a->config->gitmodules_sha1, b->config->gitmodules_sha1);
}

Project: git
File: submodule-config.c

static int parse_push_recurse(const char *opt, const char *arg,
			       int die_on_error)
{
	switch (git_config_maybe_bool(opt, arg)) {
	case 1:
		/* There's no simple "on" value when pushing */
		if (die_on_error)
			die("bad %s argument: %s", opt, arg);
		else
			return RECURSE_SUBMODULES_ERROR;
	case 0:
		return RECURSE_SUBMODULES_OFF;
	default:
		if (!strcmp(arg, "on-demand"))
			return RECURSE_SUBMODULES_ON_DEMAND;
		else if (!strcmp(arg, "check"))
			return RECURSE_SUBMODULES_CHECK;
		else if (!strcmp(arg, "only"))
			return RECURSE_SUBMODULES_ONLY;
		else if (die_on_error)
			die("bad %s argument: %s", opt, arg);
		else
			return RECURSE_SUBMODULES_ERROR;
	}
}

Project: git
File: submodule-config.c

static const struct submodule *config_from(struct submodule_cache *cache,
		const unsigned char *treeish_name, const char *key,
		enum lookup_type lookup_type)
{
	struct strbuf rev = STRBUF_INIT;
	unsigned long config_size;
	char *config = NULL;
	unsigned char sha1[20];
	enum object_type type;
	const struct submodule *submodule = NULL;
	struct parse_config_parameter parameter;

	/*
	 * If any parameter except the cache is a NULL pointer just
	 * return the first submodule. Can be used to check whether
	 * there are any submodules parsed.
	 */
	if (!treeish_name || !key) {
		struct hashmap_iter iter;
		struct submodule_entry *entry;

		entry = hashmap_iter_first(&cache->for_name, &iter);
		if (!entry)
			return NULL;
		return entry->config;
	}

	if (!gitmodule_sha1_from_commit(treeish_name, sha1, &rev))
		goto out;

	switch (lookup_type) {
	case lookup_name:
		submodule = cache_lookup_name(cache, sha1, key);
		break;
	case lookup_path:
		submodule = cache_lookup_path(cache, sha1, key);
		break;
	}
	if (submodule)
		goto out;

	config = read_sha1_file(sha1, &type, &config_size);
	if (!config || type != OBJ_BLOB)
		goto out;

	/* fill the submodule config into the cache */
	parameter.cache = cache;
	parameter.treeish_name = treeish_name;
	parameter.gitmodules_sha1 = sha1;
	parameter.overwrite = 0;
	git_config_from_mem(parse_config, CONFIG_ORIGIN_SUBMODULE_BLOB, rev.buf,
			config, config_size, &parameter);
	strbuf_release(&rev);
	free(config);

	switch (lookup_type) {
	case lookup_name:
		return cache_lookup_name(cache, sha1, key);
	case lookup_path:
		return cache_lookup_path(cache, sha1, key);
	default:
		return NULL;
	}

out:
	strbuf_release(&rev);
	free(config);
	return submodule;
}

Project: git
File: submodule-config.c

static void submodule_cache_init(struct submodule_cache *cache)
{
	hashmap_init(&cache->for_path, (hashmap_cmp_fn) config_path_cmp, NULL, 0);
	hashmap_init(&cache->for_name, (hashmap_cmp_fn) config_name_cmp, NULL, 0);
	cache->initialized = 1;
}

Project: git
File: submodule-config.c

const struct submodule *submodule_from_name(const unsigned char *treeish_name,
		const char *name)
{
	submodule_cache_check_init(the_repository);
	return config_from(the_repository->submodule_cache, treeish_name, name, lookup_name);
}

Project: git
File: submodule-config.c

const struct submodule *submodule_from_path(const unsigned char *treeish_name,
		const char *path)
{
	submodule_cache_check_init(the_repository);
	return config_from(the_repository->submodule_cache, treeish_name, path, lookup_path);
}

Project: git
File: submodule-config.c

static int parse_fetch_recurse(const char *opt, const char *arg,
			       int die_on_error)
{
	switch (git_config_maybe_bool(opt, arg)) {
	case 1:
		return RECURSE_SUBMODULES_ON;
	case 0:
		return RECURSE_SUBMODULES_OFF;
	default:
		if (!strcmp(arg, "on-demand"))
			return RECURSE_SUBMODULES_ON_DEMAND;

		if (die_on_error)
			die("bad %s argument: %s", opt, arg);
		else
			return RECURSE_SUBMODULES_ERROR;
	}
}

Project: git
File: submodule-config.c

static int config_name_cmp(const void *unused_cmp_data,
			   const struct submodule_entry *a,
			   const struct submodule_entry *b,
			   const void *unused_keydata)
{
	return strcmp(a->config->name, b->config->name) ||
	       hashcmp(a->config->gitmodules_sha1, b->config->gitmodules_sha1);
}

Project: git
File: submodule-config.c

const struct submodule *submodule_from_cache(struct repository *repo,
					     const unsigned char *treeish_name,
					     const char *key)
{
	submodule_cache_check_init(repo);
	return config_from(repo->submodule_cache, treeish_name,
			   key, lookup_path);
}