diff -ur b/credential.c a/credential.c --- b/credential.c 2020-04-09 09:53:10.568938942 +0200 +++ a/credential.c 2020-04-09 12:34:56.735418779 +0200 @@ -195,6 +195,8 @@ { if (!value) return; + if (strchr(value, '\n')) + die("credential value for %s contains newline", key); fprintf(fp, "%s=%s\n", key, value); } @@ -321,8 +323,21 @@ FREE_AND_NULL(c->password); c->approved = 0; } - -void credential_from_url(struct credential *c, const char *url) +static int check_url_component(const char *url, int quiet, + const char *name, const char *value) +{ + if (!value) + return 0; + if (!strchr(value, '\n')) + return 0; + + if (!quiet) + warning(_("url contains a newline in its %s component: %s"), + name, url); + return -1; +} +int credential_from_url_gently(struct credential *c, const char *url, + int quiet) { const char *at, *colon, *cp, *slash, *host, *proto_end; @@ -336,7 +351,7 @@ */ proto_end = strstr(url, "://"); if (!proto_end) - return; + return 0; cp = proto_end + 3; at = strchr(cp, '@'); colon = strchr(cp, ':'); @@ -371,4 +386,21 @@ while (p > c->path && *p == '/') *p-- = '\0'; } + + if (check_url_component(url, quiet, "username", c->username) < 0 || + check_url_component(url, quiet, "password", c->password) < 0 || + check_url_component(url, quiet, "protocol", c->protocol) < 0 || + check_url_component(url, quiet, "host", c->host) < 0 || + check_url_component(url, quiet, "path", c->path) < 0) + return -1; + + return 0; +} + +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); + } } diff -ur b/credential.h a/credential.h --- b/credential.h 2020-04-09 09:53:10.568938942 +0200 +++ a/credential.h 2020-04-09 10:06:07.467159059 +0200 @@ -28,7 +28,23 @@ int credential_read(struct credential *, FILE *); void credential_write(const struct credential *, FILE *); + +/* + * Parse a url into a credential struct, replacing any existing contents. + * + * Ifthe url can't be parsed (e.g., a missing "proto://" component), the + * resulting credential will be empty but we'll still return success from the + * "gently" form. + * + * If we encounter a component which cannot be represented as a credential + * value (e.g., because it contains a newline), the "gently" form will return + * an error but leave the broken state in the credential object for further + * examination. The non-gentle form will issue a warning to stderr and return + * an empty credential. +*/ void credential_from_url(struct credential *, const char *url); +int credential_from_url_gently(struct credential *, const char *url, int quiet); + int credential_match(const struct credential *have, const struct credential *want); diff -ur b/fsck.c a/fsck.c --- b/fsck.c 2020-04-09 09:53:10.569938954 +0200 +++ a/fsck.c 2020-04-09 12:29:42.713615414 +0200 @@ -14,6 +14,7 @@ #include "packfile.h" #include "submodule-config.h" #include "config.h" +#include "credential.h" static struct oidset gitmodules_found = OIDSET_INIT; static struct oidset gitmodules_done = OIDSET_INIT; @@ -945,6 +946,19 @@ return fsck_tag_buffer(tag, data, size, options); } +static int check_submodule_url(const char *url) +{ + struct credential c = CREDENTIAL_INIT; + int ret; + + if (looks_like_command_line_option(url)) + return -1; + + ret = credential_from_url_gently(&c, url, 1); + credential_clear(&c); + return ret; +} + struct fsck_gitmodules_data { struct object *obj; struct fsck_options *options; @@ -969,8 +983,8 @@ "disallowed submodule name: %s", name); if (!strcmp(key, "url") && value && - looks_like_command_line_option(value)) - data->ret |= report(data->options, data->obj, + check_submodule_url(value) < 0) + data->ret |= report(data->options, data->obj, FSCK_MSG_GITMODULES_URL, "disallowed submodule url: %s", value); diff -ur b/t/lib-credential.sh a/t/lib-credential.sh --- b/t/lib-credential.sh 2020-04-09 09:53:10.501938148 +0200 +++ a/t/lib-credential.sh 2020-04-09 12:28:10.369496961 +0200 @@ -19,7 +19,7 @@ false fi && test_cmp expect-stdout stdout && - test_cmp expect-stderr stderr + test_i18ncmp expect-stderr stderr } read_chunk() { diff -ur b/t/t0300-credentials.sh a/t/t0300-credentials.sh --- b/t/t0300-credentials.sh 2020-04-09 09:53:10.506938208 +0200 +++ a/t/t0300-credentials.sh 2020-04-09 12:26:42.660434645 +0200 @@ -309,4 +309,18 @@ EOF ' +test_expect_success 'url parser ignores embedded newlines' ' + check fill <<-EOF + url=https://one.example.com?%0ahost=two.example.com/ + -- + username=askpass-username + password=askpass-password + -- + 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: + EOF +' + test_done diff -ur b/t/t7416-submodule-dash-url.sh a/t/t7416-submodule-dash-url.sh --- b/t/t7416-submodule-dash-url.sh 2020-04-09 09:53:10.528938468 +0200 +++ a/t/t7416-submodule-dash-url.sh 2020-04-09 12:27:15.594833539 +0200 @@ -1,6 +1,6 @@ #!/bin/sh -test_description='check handling of .gitmodule url with dash' +test_description='check handling of disallowed .gitmodule urls' . ./test-lib.sh test_expect_success 'create submodule with protected dash in url' ' @@ -60,4 +60,19 @@ test_i18ngrep ! "unknown option" err ' +test_expect_success 'fsck rejects embedded newline in url' ' + git checkout --orphan newline && + cat >.gitmodules <<-\EOF && + [submodule "foo"] + url = "https://one.example.com?%0ahost=two.example.com/foo.git" + EOF + git add .gitmodules && + git commit -m "gitmodules 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