aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon McVittie <smcv@debian.org>2016-12-24 15:58:10 +0000
committerSimon McVittie <smcv@debian.org>2016-12-28 21:32:12 +0000
commitad04dac19b1fbba33cbb7dadb17ae0ef0423b2e1 (patch)
tree54c2cb842cfcd6784453f5fe5ea79670c19a09dd
parenta8a7462382ff235086743f06a92a9ab9100083b4 (diff)
downloadikiwiki-ad04dac19b1fbba33cbb7dadb17ae0ef0423b2e1.tar
ikiwiki-ad04dac19b1fbba33cbb7dadb17ae0ef0423b2e1.tar.gz
Add automated test for using the CGI with git, including CVE-2016-10026
-rw-r--r--debian/changelog3
-rw-r--r--debian/control1
-rwxr-xr-xt/git-cgi.t300
3 files changed, 304 insertions, 0 deletions
diff --git a/debian/changelog b/debian/changelog
index b057ec7f2..c7d193825 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -13,6 +13,9 @@ ikiwiki (3.20161220) UNRELEASED; urgency=medium
(CVE-2016-10026 represents the original vulnerability)
(OVE-20161226-0002 represents the incomplete fix released in 3.20161219)
* Add CVE references for CVE-2016-10026
+ * Add automated test for using the CGI with git, including
+ CVE-2016-10026
+ - Build-depend on libipc-run-perl for better build-time test coverage
* Add missing ikiwiki.setup for the manual test for CVE-2016-10026
* git: don't issue a warning if the rcsinfo CGI parameter is undefined
* git: do not fail to commit changes with a recent git version
diff --git a/debian/control b/debian/control
index 9f228c75b..47abb9a29 100644
--- a/debian/control
+++ b/debian/control
@@ -16,6 +16,7 @@ Build-Depends-Indep:
libhtml-parser-perl,
libhtml-scrubber-perl,
libhtml-template-perl,
+ libipc-run-perl,
libimage-magick-perl | perlmagick,
libmagickcore-extra,
libnet-openid-consumer-perl,
diff --git a/t/git-cgi.t b/t/git-cgi.t
new file mode 100755
index 000000000..7058c33e3
--- /dev/null
+++ b/t/git-cgi.t
@@ -0,0 +1,300 @@
+#!/usr/bin/perl
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN {
+ my $git = `which git`;
+ chomp $git;
+ plan(skip_all => 'git not available') unless -x $git;
+
+ plan(skip_all => "CGI not available")
+ unless eval q{
+ use CGI qw();
+ 1;
+ };
+
+ plan(skip_all => "IPC::Run not available")
+ unless eval q{
+ use IPC::Run qw(run);
+ 1;
+ };
+
+ use_ok('IkiWiki');
+ use_ok('YAML::XS');
+}
+
+# We check for English error messages
+$ENV{LC_ALL} = 'C';
+
+use Cwd qw(getcwd);
+use Errno qw(ENOENT);
+
+my $installed = $ENV{INSTALLED_TESTS};
+
+my @command;
+if ($installed) {
+ @command = qw(ikiwiki);
+}
+else {
+ ok(! system("make -s ikiwiki.out"));
+ @command = ("perl", "-I".getcwd."/blib/lib", './ikiwiki.out',
+ '--underlaydir='.getcwd.'/underlays/basewiki',
+ '--set', 'underlaydirbase='.getcwd.'/underlays',
+ '--templatedir='.getcwd.'/templates');
+}
+
+sub write_old_file {
+ my $name = shift;
+ my $dir = shift;
+ my $content = shift;
+ writefile($name, $dir, $content);
+ ok(utime(333333333, 333333333, "$dir/$name"));
+}
+
+sub write_setup_file {
+ my %setup = (
+ wikiname => 'this is the name of my wiki',
+ srcdir => getcwd.'/t/tmp/in/doc',
+ destdir => getcwd.'/t/tmp/out',
+ url => 'http://example.com',
+ cgiurl => 'http://example.com/cgi-bin/ikiwiki.cgi',
+ cgi_wrapper => getcwd.'/t/tmp/ikiwiki.cgi',
+ cgi_wrappermode => '0751',
+ add_plugins => [qw(anonok lockedit recentchanges)],
+ disable_plugins => [qw(emailauth openid passwordauth)],
+ anonok_pagespec => 'writable/*',
+ locked_pages => '!writable/*',
+ rcs => 'git',
+ git_wrapper => getcwd.'/t/tmp/in/.git/hooks/post-commit',
+ git_wrappermode => '0754',
+ gitorigin_branch => '',
+ );
+ unless ($installed) {
+ $setup{ENV} = { 'PERL5LIB' => getcwd.'/blib/lib' };
+ }
+ writefile("test.setup", "t/tmp",
+ "# IkiWiki::Setup::Yaml - YAML formatted setup file\n" .
+ Dump(\%setup));
+}
+
+sub thoroughly_rebuild {
+ ok(unlink("t/tmp/ikiwiki.cgi") || $!{ENOENT});
+ ok(unlink("t/tmp/in/.git/hooks/post-commit") || $!{ENOENT});
+ ok(! system(@command, qw(--setup t/tmp/test.setup --rebuild --wrappers)));
+}
+
+sub check_cgi_mode_bits {
+ my $mode;
+
+ (undef, undef, $mode, undef, undef,
+ undef, undef, undef, undef, undef,
+ undef, undef, undef) = stat('t/tmp/ikiwiki.cgi');
+ is ($mode & 07777, 0751);
+ (undef, undef, $mode, undef, undef,
+ undef, undef, undef, undef, undef,
+ undef, undef, undef) = stat('t/tmp/in/.git/hooks/post-commit');
+ is ($mode & 07777, 0754);
+}
+
+sub run_cgi {
+ my (%args) = @_;
+ my ($in, $out);
+ my $method = $args{method} || 'GET';
+ my $environ = $args{environ} || {};
+ my $params = $args{params} || { do => 'prefs' };
+
+ my %defaults = (
+ SCRIPT_NAME => '/cgi-bin/ikiwiki.cgi',
+ HTTP_HOST => 'example.com',
+ );
+
+ my $cgi = CGI->new($args{params});
+ my $query_string = $cgi->query_string();
+
+ if ($method eq 'POST') {
+ $defaults{REQUEST_METHOD} = 'POST';
+ $in = $query_string;
+ $defaults{CONTENT_LENGTH} = length $in;
+ } else {
+ $defaults{REQUEST_METHOD} = 'GET';
+ $defaults{QUERY_STRING} = $query_string;
+ }
+
+ my %envvars = (
+ %defaults,
+ %$environ,
+ );
+ run(["./t/tmp/ikiwiki.cgi"], \$in, \$out, init => sub {
+ map {
+ $ENV{$_} = $envvars{$_}
+ } keys(%envvars);
+ });
+
+ return $out;
+}
+
+sub run_git {
+ my (undef, $filename, $line) = caller;
+ my $args = shift;
+ my $desc = shift || join(' ', 'git', @$args);
+ my ($in, $out);
+ ok(run(['git', @$args], \$in, \$out, init => sub {
+ chdir 't/tmp/in' or die $!;
+ $ENV{EMAIL} = 'nobody@ikiwiki-tests.invalid';
+ }), "$desc at $filename:$line");
+ return $out;
+}
+
+sub test {
+ my $content;
+ my $status;
+
+ ok(! system(qw(rm -rf t/tmp)));
+ ok(! system(qw(mkdir t/tmp)));
+
+ write_old_file('.gitignore', 't/tmp/in', "/doc/.ikiwiki/\n");
+ write_old_file('doc/writable/one.mdwn', 't/tmp/in', 'This is the first test page');
+ write_old_file('doc/writable/two.mdwn', 't/tmp/in', 'This is the second test page');
+ write_old_file('doc/writable/three.mdwn', 't/tmp/in', 'This is the third test page');
+
+ unless ($installed) {
+ ok(! system(qw(cp -pRL doc/wikiicons t/tmp/in/doc/)));
+ ok(! system(qw(cp -pRL doc/recentchanges.mdwn t/tmp/in/doc/)));
+ }
+
+ run_git(['init']);
+ run_git(['add', '.']);
+ run_git(['commit', '-m', 'Initial commit']);
+
+ write_setup_file();
+ thoroughly_rebuild();
+ check_cgi_mode_bits();
+
+ ok(-e 't/tmp/out/writable/one/index.html');
+ $content = readfile('t/tmp/out/writable/one/index.html');
+ like($content, qr{This is the first test page});
+ my $orig_sha1 = run_git(['rev-list', '--max-count=1', 'HEAD']);
+
+ # Test the git hook, which accepts git commits
+ writefile('doc/writable/one.mdwn', 't/tmp/in',
+ 'This is new content for the first test page');
+ run_git(['add', '.']);
+ run_git(['commit', '-m', 'Git commit']);
+ my $first_revertable_sha1 = run_git(['rev-list', '--max-count=1', 'HEAD']);
+ isnt($orig_sha1, $first_revertable_sha1);
+
+ ok(-e 't/tmp/out/writable/one/index.html');
+ $content = readfile('t/tmp/out/writable/one/index.html');
+ like($content, qr{This is new content for the first test page});
+
+ # Test a web commit
+ $content = run_cgi(method => 'POST',
+ params => {
+ do => 'edit',
+ page => 'writable/two',
+ type => 'mdwn',
+ editmessage => 'Web commit',
+ editcontent => 'Here is new content for the second page',
+ _submit => 'Save Page',
+ _submitted => '1',
+ },
+ );
+ like($content, qr{^Status:\s*302\s}m);
+ like($content, qr{^Location:\s*http://example\.com/writable/two/\?updated}m);
+ my $second_revertable_sha1 = run_git(['rev-list', '--max-count=1', 'HEAD']);
+ isnt($orig_sha1, $second_revertable_sha1);
+ isnt($first_revertable_sha1, $second_revertable_sha1);
+
+ ok(-e 't/tmp/out/writable/two/index.html');
+ $content = readfile('t/tmp/out/writable/two/index.html');
+ like($content, qr{Here is new content for the second page});
+
+ # Another edit
+ writefile('doc/writable/three.mdwn', 't/tmp/in',
+ 'Also new content for the third page');
+ run_git(['add', '.']);
+ run_git(['commit', '-m', 'Git commit']);
+ ok(-e 't/tmp/out/writable/three/index.html');
+ $content = readfile('t/tmp/out/writable/three/index.html');
+ like($content, qr{Also new content for the third page});
+ my $third_revertable_sha1 = run_git(['rev-list', '--max-count=1', 'HEAD']);
+ isnt($orig_sha1, $third_revertable_sha1);
+ isnt($second_revertable_sha1, $third_revertable_sha1);
+
+ run_git(['mv', 'doc/writable/one.mdwn', 'doc/one.mdwn']);
+ run_git(['mv', 'doc/writable/two.mdwn', 'two.mdwn']);
+ run_git(['commit', '-m', 'Rename files to test CVE-2016-10026']);
+ ok(! -e 't/tmp/out/writable/two/index.html');
+ ok(! -e 't/tmp/out/writable/one/index.html');
+ ok(-e 't/tmp/out/one/index.html');
+ my $sha1_before_revert = run_git(['rev-list', '--max-count=1', 'HEAD']);
+ isnt($sha1_before_revert, $third_revertable_sha1);
+
+ $content = run_cgi(method => 'post',
+ params => {
+ do => 'revert',
+ revertmessage => 'CVE-2016-10026',
+ rev => $first_revertable_sha1,
+ _submit => 'Revert',
+ _submitted_revert => '1',
+ },
+ );
+ like($content, qr{is locked and cannot be edited});
+ # The tree is left clean
+ run_git(['diff', '--exit-code']);
+ run_git(['diff', '--cached', '--exit-code']);
+ my $sha1 = run_git(['rev-list', '--max-count=1', 'HEAD']);
+ is($sha1, $sha1_before_revert);
+
+ ok(-e 't/tmp/out/one/index.html');
+ ok(! -e 't/tmp/in/doc/writable/one.mdwn');
+ ok(-e 't/tmp/in/doc/one.mdwn');
+ $content = readfile('t/tmp/out/one/index.html');
+ like($content, qr{This is new content for the first test page});
+
+ $content = run_cgi(method => 'post',
+ params => {
+ do => 'revert',
+ revertmessage => 'CVE-2016-10026',
+ rev => $second_revertable_sha1,
+ _submit => 'Revert',
+ _submitted_revert => '1',
+ },
+ );
+ like($content, qr{you are not allowed to change two\.mdwn});
+ run_git(['diff', '--exit-code']);
+ run_git(['diff', '--cached', '--exit-code']);
+ $sha1 = run_git(['rev-list', '--max-count=1', 'HEAD']);
+ is($sha1, $sha1_before_revert);
+
+ ok(! -e 't/tmp/out/writable/two/index.html');
+ ok(! -e 't/tmp/out/two/index.html');
+ ok(! -e 't/tmp/in/doc/writable/two.mdwn');
+ ok(-e 't/tmp/in/two.mdwn');
+ $content = readfile('t/tmp/in/two.mdwn');
+ like($content, qr{Here is new content for the second page});
+
+ # This one can legitimately be reverted
+ $content = run_cgi(method => 'post',
+ params => {
+ do => 'revert',
+ revertmessage => 'not CVE-2016-10026',
+ rev => $third_revertable_sha1,
+ _submit => 'Revert',
+ _submitted_revert => '1',
+ },
+ );
+ like($content, qr{^Status:\s*302\s}m);
+ like($content, qr{^Location:\s*http://example\.com/recentchanges/}m);
+ run_git(['diff', '--exit-code']);
+ run_git(['diff', '--cached', '--exit-code']);
+ ok(-e 't/tmp/out/writable/three/index.html');
+ $content = readfile('t/tmp/out/writable/three/index.html');
+ like($content, qr{This is the third test page});
+}
+
+test();
+
+done_testing();