summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar Jonathan Nieder <jrnieder@gmail.com>2020-04-19 16:30:08 -0700
committerLibravatar Jonathan Nieder <jrnieder@gmail.com>2020-04-19 16:30:08 -0700
commit9206d27eb5e76bbb3a4e59e3c53f92eaa0caa97b (patch)
treec7bf92c51265916390430f79f5233e33be25b4ed
parentGit 2.21.2 (diff)
parentGit 2.20.4 (diff)
downloadtgif-9206d27eb5e76bbb3a4e59e3c53f92eaa0caa97b.tar.xz
Git 2.21.3
This merges up the security fix from v2.17.5. Signed-off-by: Jonathan Nieder <jrnieder@gmail.com>
-rw-r--r--Documentation/RelNotes/2.17.5.txt22
-rw-r--r--Documentation/RelNotes/2.18.4.txt5
-rw-r--r--Documentation/RelNotes/2.19.5.txt5
-rw-r--r--Documentation/RelNotes/2.20.4.txt5
-rw-r--r--Documentation/RelNotes/2.21.3.txt5
-rwxr-xr-xGIT-VERSION-GEN2
l---------RelNotes2
-rw-r--r--credential.c39
-rw-r--r--fsck.c141
-rw-r--r--http.c1
-rwxr-xr-xt/t0300-credentials.sh153
-rwxr-xr-xt/t5550-http-fetch-dumb.sh16
-rwxr-xr-xt/t7416-submodule-dash-url.sh125
13 files changed, 479 insertions, 42 deletions
diff --git a/Documentation/RelNotes/2.17.5.txt b/Documentation/RelNotes/2.17.5.txt
new file mode 100644
index 0000000000..2abb821a73
--- /dev/null
+++ b/Documentation/RelNotes/2.17.5.txt
@@ -0,0 +1,22 @@
+Git v2.17.5 Release Notes
+=========================
+
+This release is to address a security issue: CVE-2020-11008
+
+Fixes since v2.17.4
+-------------------
+
+ * With a crafted URL that contains a newline or empty host, or lacks
+ a scheme, the credential helper machinery can be fooled into
+ providing credential information that is not appropriate for the
+ protocol in use and host being contacted.
+
+ Unlike the vulnerability CVE-2020-5260 fixed in v2.17.4, the
+ credentials are not for a host of the attacker's choosing; instead,
+ they are for some unspecified host (based on how the configured
+ credential helper handles an absent "host" parameter).
+
+ The attack has been made impossible by refusing to work with
+ under-specified credential patterns.
+
+Credit for finding the vulnerability goes to Carlo Arenas.
diff --git a/Documentation/RelNotes/2.18.4.txt b/Documentation/RelNotes/2.18.4.txt
new file mode 100644
index 0000000000..e8ef858a00
--- /dev/null
+++ b/Documentation/RelNotes/2.18.4.txt
@@ -0,0 +1,5 @@
+Git v2.18.4 Release Notes
+=========================
+
+This release merges the security fix that appears in v2.17.5; see
+the release notes for that version for details.
diff --git a/Documentation/RelNotes/2.19.5.txt b/Documentation/RelNotes/2.19.5.txt
new file mode 100644
index 0000000000..18a4dcbfd6
--- /dev/null
+++ b/Documentation/RelNotes/2.19.5.txt
@@ -0,0 +1,5 @@
+Git v2.19.5 Release Notes
+=========================
+
+This release merges the security fix that appears in v2.17.5; see
+the release notes for that version for details.
diff --git a/Documentation/RelNotes/2.20.4.txt b/Documentation/RelNotes/2.20.4.txt
new file mode 100644
index 0000000000..5a9e24e470
--- /dev/null
+++ b/Documentation/RelNotes/2.20.4.txt
@@ -0,0 +1,5 @@
+Git v2.20.4 Release Notes
+=========================
+
+This release merges the security fix that appears in v2.17.5; see
+the release notes for that version for details.
diff --git a/Documentation/RelNotes/2.21.3.txt b/Documentation/RelNotes/2.21.3.txt
new file mode 100644
index 0000000000..2ca0aa5c62
--- /dev/null
+++ b/Documentation/RelNotes/2.21.3.txt
@@ -0,0 +1,5 @@
+Git v2.21.3 Release Notes
+=========================
+
+This release merges the security fix that appears in v2.17.5; see
+the release notes for that version for details.
diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN
index 9fa5ebbcea..372758ca62 100755
--- a/GIT-VERSION-GEN
+++ b/GIT-VERSION-GEN
@@ -1,7 +1,7 @@
#!/bin/sh
GVF=GIT-VERSION-FILE
-DEF_VER=v2.21.2
+DEF_VER=v2.21.3
LF='
'
diff --git a/RelNotes b/RelNotes
index ce5750f047..19f6240eec 120000
--- a/RelNotes
+++ b/RelNotes
@@ -1 +1 @@
-Documentation/RelNotes/2.21.2.txt \ No newline at end of file
+Documentation/RelNotes/2.21.3.txt \ No newline at end of file
diff --git a/credential.c b/credential.c
index 24823829f6..cf11cc98f4 100644
--- a/credential.c
+++ b/credential.c
@@ -89,6 +89,11 @@ static int proto_is_http(const char *s)
static void credential_apply_config(struct credential *c)
{
+ if (!c->host)
+ die(_("refusing to work with credential missing host field"));
+ if (!c->protocol)
+ die(_("refusing to work with credential missing protocol field"));
+
if (c->configured)
return;
git_config(credential_config_callback, c);
@@ -191,8 +196,11 @@ int credential_read(struct credential *c, FILE *fp)
return 0;
}
-static void credential_write_item(FILE *fp, const char *key, const char *value)
+static void credential_write_item(FILE *fp, const char *key, const char *value,
+ int required)
{
+ if (!value && required)
+ BUG("credential value for %s is missing", key);
if (!value)
return;
if (strchr(value, '\n'))
@@ -202,11 +210,11 @@ static void credential_write_item(FILE *fp, const char *key, const char *value)
void credential_write(const struct credential *c, FILE *fp)
{
- credential_write_item(fp, "protocol", c->protocol);
- credential_write_item(fp, "host", c->host);
- credential_write_item(fp, "path", c->path);
- credential_write_item(fp, "username", c->username);
- credential_write_item(fp, "password", c->password);
+ credential_write_item(fp, "protocol", c->protocol, 1);
+ credential_write_item(fp, "host", c->host, 1);
+ credential_write_item(fp, "path", c->path, 0);
+ credential_write_item(fp, "username", c->username, 0);
+ credential_write_item(fp, "password", c->password, 0);
}
static int run_credential_helper(struct credential *c,
@@ -352,8 +360,11 @@ int credential_from_url_gently(struct credential *c, const char *url,
* (3) proto://<user>:<pass>@<host>/...
*/
proto_end = strstr(url, "://");
- if (!proto_end)
- return 0;
+ if (!proto_end || proto_end == url) {
+ if (!quiet)
+ warning(_("url has no scheme: %s"), url);
+ return -1;
+ }
cp = proto_end + 3;
at = strchr(cp, '@');
colon = strchr(cp, ':');
@@ -374,10 +385,8 @@ int credential_from_url_gently(struct credential *c, const char *url,
host = at + 1;
}
- if (proto_end - url > 0)
- c->protocol = xmemdupz(url, proto_end - url);
- if (slash - host > 0)
- c->host = url_decode_mem(host, slash - host);
+ c->protocol = xmemdupz(url, proto_end - url);
+ c->host = url_decode_mem(host, slash - host);
/* Trim leading and trailing slashes from path */
while (*slash == '/')
slash++;
@@ -401,8 +410,6 @@ int credential_from_url_gently(struct credential *c, const char *url,
void credential_from_url(struct credential *c, const char *url)
{
- if (credential_from_url_gently(c, url, 0) < 0) {
- warning(_("skipping credential lookup for url: %s"), url);
- credential_clear(c);
- }
+ if (credential_from_url_gently(c, url, 0) < 0)
+ die(_("credential url cannot be parsed: %s"), url);
}
diff --git a/fsck.c b/fsck.c
index 6e5b3af6e8..72e3c1911f 100644
--- a/fsck.c
+++ b/fsck.c
@@ -9,6 +9,7 @@
#include "tag.h"
#include "fsck.h"
#include "refs.h"
+#include "url.h"
#include "utf8.h"
#include "decorate.h"
#include "oidset.h"
@@ -983,17 +984,147 @@ static int fsck_tag(struct tag *tag, const char *data,
return fsck_tag_buffer(tag, data, size, options);
}
+/*
+ * Like builtin/submodule--helper.c's starts_with_dot_slash, but without
+ * relying on the platform-dependent is_dir_sep helper.
+ *
+ * This is for use in checking whether a submodule URL is interpreted as
+ * relative to the current directory on any platform, since \ is a
+ * directory separator on Windows but not on other platforms.
+ */
+static int starts_with_dot_slash(const char *str)
+{
+ return str[0] == '.' && (str[1] == '/' || str[1] == '\\');
+}
+
+/*
+ * Like starts_with_dot_slash, this is a variant of submodule--helper's
+ * helper of the same name with the twist that it accepts backslash as a
+ * directory separator even on non-Windows platforms.
+ */
+static int starts_with_dot_dot_slash(const char *str)
+{
+ return str[0] == '.' && starts_with_dot_slash(str + 1);
+}
+
+static int submodule_url_is_relative(const char *url)
+{
+ return starts_with_dot_slash(url) || starts_with_dot_dot_slash(url);
+}
+
+/*
+ * Count directory components that a relative submodule URL should chop
+ * from the remote_url it is to be resolved against.
+ *
+ * In other words, this counts "../" components at the start of a
+ * submodule URL.
+ *
+ * Returns the number of directory components to chop and writes a
+ * pointer to the next character of url after all leading "./" and
+ * "../" components to out.
+ */
+static int count_leading_dotdots(const char *url, const char **out)
+{
+ int result = 0;
+ while (1) {
+ if (starts_with_dot_dot_slash(url)) {
+ result++;
+ url += strlen("../");
+ continue;
+ }
+ if (starts_with_dot_slash(url)) {
+ url += strlen("./");
+ continue;
+ }
+ *out = url;
+ return result;
+ }
+}
+/*
+ * Check whether a transport is implemented by git-remote-curl.
+ *
+ * If it is, returns 1 and writes the URL that would be passed to
+ * git-remote-curl to the "out" parameter.
+ *
+ * Otherwise, returns 0 and leaves "out" untouched.
+ *
+ * Examples:
+ * http::https://example.com/repo.git -> 1, https://example.com/repo.git
+ * https://example.com/repo.git -> 1, https://example.com/repo.git
+ * git://example.com/repo.git -> 0
+ *
+ * This is for use in checking for previously exploitable bugs that
+ * required a submodule URL to be passed to git-remote-curl.
+ */
+static int url_to_curl_url(const char *url, const char **out)
+{
+ /*
+ * We don't need to check for case-aliases, "http.exe", and so
+ * on because in the default configuration, is_transport_allowed
+ * prevents URLs with those schemes from being cloned
+ * automatically.
+ */
+ if (skip_prefix(url, "http::", out) ||
+ skip_prefix(url, "https::", out) ||
+ skip_prefix(url, "ftp::", out) ||
+ skip_prefix(url, "ftps::", out))
+ return 1;
+ if (starts_with(url, "http://") ||
+ starts_with(url, "https://") ||
+ starts_with(url, "ftp://") ||
+ starts_with(url, "ftps://")) {
+ *out = url;
+ return 1;
+ }
+ return 0;
+}
+
static int check_submodule_url(const char *url)
{
- struct credential c = CREDENTIAL_INIT;
- int ret;
+ const char *curl_url;
if (looks_like_command_line_option(url))
return -1;
- ret = credential_from_url_gently(&c, url, 1);
- credential_clear(&c);
- return ret;
+ if (submodule_url_is_relative(url)) {
+ char *decoded;
+ const char *next;
+ int has_nl;
+
+ /*
+ * This could be appended to an http URL and url-decoded;
+ * check for malicious characters.
+ */
+ decoded = url_decode(url);
+ has_nl = !!strchr(decoded, '\n');
+
+ free(decoded);
+ if (has_nl)
+ return -1;
+
+ /*
+ * URLs which escape their root via "../" can overwrite
+ * the host field and previous components, resolving to
+ * URLs like https::example.com/submodule.git and
+ * https:///example.com/submodule.git that were
+ * susceptible to CVE-2020-11008.
+ */
+ if (count_leading_dotdots(url, &next) > 0 &&
+ (*next == ':' || *next == '/'))
+ return -1;
+ }
+
+ else if (url_to_curl_url(url, &curl_url)) {
+ struct credential c = CREDENTIAL_INIT;
+ int ret = 0;
+ if (credential_from_url_gently(&c, curl_url, 1) ||
+ !*c.host)
+ ret = -1;
+ credential_clear(&c);
+ return ret;
+ }
+
+ return 0;
}
struct fsck_gitmodules_data {
diff --git a/http.c b/http.c
index a32ad36ddf..2b8136275a 100644
--- a/http.c
+++ b/http.c
@@ -558,6 +558,7 @@ static int has_cert_password(void)
return 0;
if (!cert_auth.password) {
cert_auth.protocol = xstrdup("cert");
+ cert_auth.host = xstrdup("");
cert_auth.username = xstrdup("");
cert_auth.path = xstrdup(ssl_cert);
credential_fill(&cert_auth);
diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh
index b9c0f1f279..6d44e7e5cc 100755
--- a/t/t0300-credentials.sh
+++ b/t/t0300-credentials.sh
@@ -22,6 +22,11 @@ test_expect_success 'setup helper scripts' '
exit 0
EOF
+ write_script git-credential-quit <<-\EOF &&
+ . ./dump
+ echo quit=1
+ EOF
+
write_script git-credential-verbatim <<-\EOF &&
user=$1; shift
pass=$1; shift
@@ -35,43 +40,71 @@ test_expect_success 'setup helper scripts' '
test_expect_success 'credential_fill invokes helper' '
check fill "verbatim foo bar" <<-\EOF
+ protocol=http
+ host=example.com
--
+ protocol=http
+ host=example.com
username=foo
password=bar
--
verbatim: get
+ verbatim: protocol=http
+ verbatim: host=example.com
EOF
'
test_expect_success 'credential_fill invokes multiple helpers' '
check fill useless "verbatim foo bar" <<-\EOF
+ protocol=http
+ host=example.com
--
+ protocol=http
+ host=example.com
username=foo
password=bar
--
useless: get
+ useless: protocol=http
+ useless: host=example.com
verbatim: get
+ verbatim: protocol=http
+ verbatim: host=example.com
EOF
'
test_expect_success 'credential_fill stops when we get a full response' '
check fill "verbatim one two" "verbatim three four" <<-\EOF
+ protocol=http
+ host=example.com
--
+ protocol=http
+ host=example.com
username=one
password=two
--
verbatim: get
+ verbatim: protocol=http
+ verbatim: host=example.com
EOF
'
test_expect_success 'credential_fill continues through partial response' '
check fill "verbatim one \"\"" "verbatim two three" <<-\EOF
+ protocol=http
+ host=example.com
--
+ protocol=http
+ host=example.com
username=two
password=three
--
verbatim: get
+ verbatim: protocol=http
+ verbatim: host=example.com
verbatim: get
+ verbatim: protocol=http
+ verbatim: host=example.com
verbatim: username=one
EOF
'
@@ -97,14 +130,20 @@ test_expect_success 'credential_fill passes along metadata' '
test_expect_success 'credential_approve calls all helpers' '
check approve useless "verbatim one two" <<-\EOF
+ protocol=http
+ host=example.com
username=foo
password=bar
--
--
useless: store
+ useless: protocol=http
+ useless: host=example.com
useless: username=foo
useless: password=bar
verbatim: store
+ verbatim: protocol=http
+ verbatim: host=example.com
verbatim: username=foo
verbatim: password=bar
EOF
@@ -112,6 +151,8 @@ test_expect_success 'credential_approve calls all helpers' '
test_expect_success 'do not bother storing password-less credential' '
check approve useless <<-\EOF
+ protocol=http
+ host=example.com
username=foo
--
--
@@ -121,14 +162,20 @@ test_expect_success 'do not bother storing password-less credential' '
test_expect_success 'credential_reject calls all helpers' '
check reject useless "verbatim one two" <<-\EOF
+ protocol=http
+ host=example.com
username=foo
password=bar
--
--
useless: erase
+ useless: protocol=http
+ useless: host=example.com
useless: username=foo
useless: password=bar
verbatim: erase
+ verbatim: protocol=http
+ verbatim: host=example.com
verbatim: username=foo
verbatim: password=bar
EOF
@@ -136,33 +183,49 @@ test_expect_success 'credential_reject calls all helpers' '
test_expect_success 'usernames can be preserved' '
check fill "verbatim \"\" three" <<-\EOF
+ protocol=http
+ host=example.com
username=one
--
+ protocol=http
+ host=example.com
username=one
password=three
--
verbatim: get
+ verbatim: protocol=http
+ verbatim: host=example.com
verbatim: username=one
EOF
'
test_expect_success 'usernames can be overridden' '
check fill "verbatim two three" <<-\EOF
+ protocol=http
+ host=example.com
username=one
--
+ protocol=http
+ host=example.com
username=two
password=three
--
verbatim: get
+ verbatim: protocol=http
+ verbatim: host=example.com
verbatim: username=one
EOF
'
test_expect_success 'do not bother completing already-full credential' '
check fill "verbatim three four" <<-\EOF
+ protocol=http
+ host=example.com
username=one
password=two
--
+ protocol=http
+ host=example.com
username=one
password=two
--
@@ -174,23 +237,31 @@ test_expect_success 'do not bother completing already-full credential' '
# askpass helper is run, we know the internal getpass is working.
test_expect_success 'empty helper list falls back to internal getpass' '
check fill <<-\EOF
+ protocol=http
+ host=example.com
--
+ protocol=http
+ host=example.com
username=askpass-username
password=askpass-password
--
- askpass: Username:
- askpass: Password:
+ askpass: Username for '\''http://example.com'\'':
+ askpass: Password for '\''http://askpass-username@example.com'\'':
EOF
'
test_expect_success 'internal getpass does not ask for known username' '
check fill <<-\EOF
+ protocol=http
+ host=example.com
username=foo
--
+ protocol=http
+ host=example.com
username=foo
password=askpass-password
--
- askpass: Password:
+ askpass: Password for '\''http://foo@example.com'\'':
EOF
'
@@ -202,7 +273,11 @@ HELPER="!f() {
test_expect_success 'respect configured credentials' '
test_config credential.helper "$HELPER" &&
check fill <<-\EOF
+ protocol=http
+ host=example.com
--
+ protocol=http
+ host=example.com
username=foo
password=bar
--
@@ -291,35 +366,85 @@ test_expect_success 'http paths can be part of context' '
test_expect_success 'helpers can abort the process' '
test_must_fail git \
- -c credential.helper="!f() { echo quit=1; }; f" \
+ -c credential.helper=quit \
-c credential.helper="verbatim foo bar" \
- credential fill >stdout &&
- test_must_be_empty stdout
+ credential fill >stdout 2>stderr <<-\EOF &&
+ protocol=http
+ host=example.com
+ EOF
+ test_must_be_empty stdout &&
+ cat >expect <<-\EOF &&
+ quit: get
+ quit: protocol=http
+ quit: host=example.com
+ fatal: credential helper '\''quit'\'' told us to quit
+ EOF
+ test_i18ncmp expect stderr
'
test_expect_success 'empty helper spec resets helper list' '
test_config credential.helper "verbatim file file" &&
check fill "" "verbatim cmdline cmdline" <<-\EOF
+ protocol=http
+ host=example.com
--
+ protocol=http
+ host=example.com
username=cmdline
password=cmdline
--
verbatim: get
+ verbatim: protocol=http
+ verbatim: host=example.com
EOF
'
-test_expect_success 'url parser ignores embedded newlines' '
- check fill <<-EOF
+test_expect_success 'url parser rejects embedded newlines' '
+ test_must_fail git credential fill 2>stderr <<-\EOF &&
url=https://one.example.com?%0ahost=two.example.com/
+ EOF
+ cat >expect <<-\EOF &&
+ warning: url contains a newline in its host component: https://one.example.com?%0ahost=two.example.com/
+ fatal: credential url cannot be parsed: https://one.example.com?%0ahost=two.example.com/
+ EOF
+ test_i18ncmp expect stderr
+'
+
+test_expect_success 'host-less URLs are parsed as empty host' '
+ check fill "verbatim foo bar" <<-\EOF
+ url=cert:///path/to/cert.pem
--
- username=askpass-username
- password=askpass-password
+ protocol=cert
+ host=
+ path=path/to/cert.pem
+ username=foo
+ password=bar
--
- warning: url contains a newline in its host component: https://one.example.com?%0ahost=two.example.com/
- warning: skipping credential lookup for url: https://one.example.com?%0ahost=two.example.com/
- askpass: Username:
- askpass: Password:
+ verbatim: get
+ verbatim: protocol=cert
+ verbatim: host=
+ verbatim: path=path/to/cert.pem
+ EOF
+'
+
+test_expect_success 'credential system refuses to work with missing host' '
+ test_must_fail git credential fill 2>stderr <<-\EOF &&
+ protocol=http
+ EOF
+ cat >expect <<-\EOF &&
+ fatal: refusing to work with credential missing host field
+ EOF
+ test_i18ncmp expect stderr
+'
+
+test_expect_success 'credential system refuses to work with missing protocol' '
+ test_must_fail git credential fill 2>stderr <<-\EOF &&
+ host=example.com
+ EOF
+ cat >expect <<-\EOF &&
+ fatal: refusing to work with credential missing protocol field
EOF
+ test_i18ncmp expect stderr
'
test_done
diff --git a/t/t5550-http-fetch-dumb.sh b/t/t5550-http-fetch-dumb.sh
index 6d7d88ccc9..bf9ca5a8ec 100755
--- a/t/t5550-http-fetch-dumb.sh
+++ b/t/t5550-http-fetch-dumb.sh
@@ -321,11 +321,17 @@ test_expect_success 'git client does not send an empty Accept-Language' '
'
test_expect_success 'remote-http complains cleanly about malformed urls' '
- # do not actually issue "list" or other commands, as we do not
- # want to rely on what curl would actually do with such a broken
- # URL. This is just about making sure we do not segfault during
- # initialization.
- test_must_fail git remote-http http::/example.com/repo.git
+ test_must_fail git remote-http http::/example.com/repo.git 2>stderr &&
+ test_i18ngrep "url has no scheme" stderr
+'
+
+# NEEDSWORK: Writing commands to git-remote-curl can race against the latter
+# erroring out, producing SIGPIPE. Remove "ok=sigpipe" once transport-helper has
+# learned to handle early remote helper failures more cleanly.
+test_expect_success 'remote-http complains cleanly about empty scheme' '
+ test_must_fail ok=sigpipe git ls-remote \
+ http::${HTTPD_URL#http}/dumb/repo.git 2>stderr &&
+ test_i18ngrep "url has no scheme" stderr
'
test_expect_success 'redirects can be forbidden/allowed' '
diff --git a/t/t7416-submodule-dash-url.sh b/t/t7416-submodule-dash-url.sh
index 41431b1ac3..eec96e0ba9 100755
--- a/t/t7416-submodule-dash-url.sh
+++ b/t/t7416-submodule-dash-url.sh
@@ -60,6 +60,116 @@ test_expect_success 'trailing backslash is handled correctly' '
test_i18ngrep ! "unknown option" err
'
+test_expect_success 'fsck rejects missing URL scheme' '
+ git checkout --orphan missing-scheme &&
+ cat >.gitmodules <<-\EOF &&
+ [submodule "foo"]
+ url = http::one.example.com/foo.git
+ EOF
+ git add .gitmodules &&
+ test_tick &&
+ git commit -m "gitmodules with missing URL scheme" &&
+ test_when_finished "rm -rf dst" &&
+ git init --bare dst &&
+ git -C dst config transfer.fsckObjects true &&
+ test_must_fail git push dst HEAD 2>err &&
+ grep gitmodulesUrl err
+'
+
+test_expect_success 'fsck rejects relative URL resolving to missing scheme' '
+ git checkout --orphan relative-missing-scheme &&
+ cat >.gitmodules <<-\EOF &&
+ [submodule "foo"]
+ url = "..\\../.\\../:one.example.com/foo.git"
+ EOF
+ git add .gitmodules &&
+ test_tick &&
+ git commit -m "gitmodules with relative URL that strips off scheme" &&
+ test_when_finished "rm -rf dst" &&
+ git init --bare dst &&
+ git -C dst config transfer.fsckObjects true &&
+ test_must_fail git push dst HEAD 2>err &&
+ grep gitmodulesUrl err
+'
+
+test_expect_success 'fsck rejects empty URL scheme' '
+ git checkout --orphan empty-scheme &&
+ cat >.gitmodules <<-\EOF &&
+ [submodule "foo"]
+ url = http::://one.example.com/foo.git
+ EOF
+ git add .gitmodules &&
+ test_tick &&
+ git commit -m "gitmodules with empty URL scheme" &&
+ test_when_finished "rm -rf dst" &&
+ git init --bare dst &&
+ git -C dst config transfer.fsckObjects true &&
+ test_must_fail git push dst HEAD 2>err &&
+ grep gitmodulesUrl err
+'
+
+test_expect_success 'fsck rejects relative URL resolving to empty scheme' '
+ git checkout --orphan relative-empty-scheme &&
+ cat >.gitmodules <<-\EOF &&
+ [submodule "foo"]
+ url = ../../../:://one.example.com/foo.git
+ EOF
+ git add .gitmodules &&
+ test_tick &&
+ git commit -m "relative gitmodules URL resolving to empty scheme" &&
+ test_when_finished "rm -rf dst" &&
+ git init --bare dst &&
+ git -C dst config transfer.fsckObjects true &&
+ test_must_fail git push dst HEAD 2>err &&
+ grep gitmodulesUrl err
+'
+
+test_expect_success 'fsck rejects empty hostname' '
+ git checkout --orphan empty-host &&
+ cat >.gitmodules <<-\EOF &&
+ [submodule "foo"]
+ url = http:///one.example.com/foo.git
+ EOF
+ git add .gitmodules &&
+ test_tick &&
+ git commit -m "gitmodules with extra slashes" &&
+ test_when_finished "rm -rf dst" &&
+ git init --bare dst &&
+ git -C dst config transfer.fsckObjects true &&
+ test_must_fail git push dst HEAD 2>err &&
+ grep gitmodulesUrl err
+'
+
+test_expect_success 'fsck rejects relative url that produced empty hostname' '
+ git checkout --orphan messy-relative &&
+ cat >.gitmodules <<-\EOF &&
+ [submodule "foo"]
+ url = ../../..//one.example.com/foo.git
+ EOF
+ git add .gitmodules &&
+ test_tick &&
+ git commit -m "gitmodules abusing relative_path" &&
+ test_when_finished "rm -rf dst" &&
+ git init --bare dst &&
+ git -C dst config transfer.fsckObjects true &&
+ test_must_fail git push dst HEAD 2>err &&
+ grep gitmodulesUrl err
+'
+
+test_expect_success 'fsck permits embedded newline with unrecognized scheme' '
+ git checkout --orphan newscheme &&
+ cat >.gitmodules <<-\EOF &&
+ [submodule "foo"]
+ url = "data://acjbkd%0akajfdickajkd"
+ EOF
+ git add .gitmodules &&
+ git commit -m "gitmodules with unrecognized scheme" &&
+ test_when_finished "rm -rf dst" &&
+ git init --bare dst &&
+ git -C dst config transfer.fsckObjects true &&
+ git push dst HEAD
+'
+
test_expect_success 'fsck rejects embedded newline in url' '
# create an orphan branch to avoid existing .gitmodules objects
git checkout --orphan newline &&
@@ -76,4 +186,19 @@ test_expect_success 'fsck rejects embedded newline in url' '
grep gitmodulesUrl err
'
+test_expect_success 'fsck rejects embedded newline in relative url' '
+ git checkout --orphan relative-newline &&
+ cat >.gitmodules <<-\EOF &&
+ [submodule "foo"]
+ url = "./%0ahost=two.example.com/foo.git"
+ EOF
+ git add .gitmodules &&
+ git commit -m "relative url with newline" &&
+ test_when_finished "rm -rf dst" &&
+ git init --bare dst &&
+ git -C dst config transfer.fsckObjects true &&
+ test_must_fail git push dst HEAD 2>err &&
+ grep gitmodulesUrl err
+'
+
test_done