[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Re: ikiwiki / CVE-2019-9187



Brian May <bam@debian.org> writes:

> Oh wait, this is a debian native package. Means I will probably have to
> patch the files directly, not rely on debian/patches. So was only
> working before because I was testing with patches applied.
>
> Curiously I am getting a test failure when testing without my patches.

Attached is the latest patch, now taking into account this is a Debian
native package without any patches applied.
-- 
Brian May <bam@debian.org>
diff -Nru ikiwiki-3.20141016.4/CHANGELOG ikiwiki-3.20141016.4+deb8u1/CHANGELOG
--- ikiwiki-3.20141016.4/CHANGELOG	2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/CHANGELOG	2019-03-07 17:35:55.000000000 +1100
@@ -1,3 +1,10 @@
+ikiwiki (3.20141016.4+deb8u1) jessie-security; urgency=high
+
+  * Non-maintainer upload by the LTS Team.
+  * CVE-2019-9187: Fix server-side request forgery via aggregate plugin.
+
+ -- Brian May <bam@debian.org>  Thu, 07 Mar 2019 17:35:55 +1100
+
 ikiwiki (3.20141016.4) jessie-security; urgency=high
 
   * Reference CVE-2016-4561 in 3.20141016.3 changelog
diff -Nru ikiwiki-3.20141016.4/debian/changelog ikiwiki-3.20141016.4+deb8u1/debian/changelog
--- ikiwiki-3.20141016.4/debian/changelog	2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/debian/changelog	2019-03-07 17:35:55.000000000 +1100
@@ -1,3 +1,10 @@
+ikiwiki (3.20141016.4+deb8u1) jessie-security; urgency=high
+
+  * Non-maintainer upload by the LTS Team.
+  * CVE-2019-9187: Fix server-side request forgery via aggregate plugin.
+
+ -- Brian May <bam@debian.org>  Thu, 07 Mar 2019 17:35:55 +1100
+
 ikiwiki (3.20141016.4) jessie-security; urgency=high
 
   * Reference CVE-2016-4561 in 3.20141016.3 changelog
diff -Nru ikiwiki-3.20141016.4/debian/control ikiwiki-3.20141016.4+deb8u1/debian/control
--- ikiwiki-3.20141016.4/debian/control	2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/debian/control	2019-03-07 17:35:55.000000000 +1100
@@ -17,7 +17,8 @@
   libnet-openid-consumer-perl,
   libxml-feed-perl,
   libxml-parser-perl,
-  libxml-twig-perl
+  libxml-twig-perl,
+  liblwpx-paranoidagent-perl,
 Maintainer: Simon McVittie <smcv@debian.org>
 Uploaders: Josh Triplett <josh@freedesktop.org>
 Standards-Version: 3.9.5
diff -Nru ikiwiki-3.20141016.4/doc/plugins/aggregate.mdwn ikiwiki-3.20141016.4+deb8u1/doc/plugins/aggregate.mdwn
--- ikiwiki-3.20141016.4/doc/plugins/aggregate.mdwn	2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/doc/plugins/aggregate.mdwn	2019-03-07 17:35:55.000000000 +1100
@@ -11,6 +11,10 @@
 one. Either the [[htmltidy]] or [[htmlbalance]] plugin is suggested, since
 feeds can easily contain html problems, some of which these plugins can fix.
 
+Installing the [[!cpan LWPx::ParanoidAgent]] Perl module is strongly
+recommended. The [[!cpan LWP]] module can also be used, but is susceptible
+to server-side request forgery.
+
 ## triggering aggregation
 
 You will need to run ikiwiki periodically from a cron job, passing it the
diff -Nru ikiwiki-3.20141016.4/doc/plugins/blogspam.mdwn ikiwiki-3.20141016.4+deb8u1/doc/plugins/blogspam.mdwn
--- ikiwiki-3.20141016.4/doc/plugins/blogspam.mdwn	2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/doc/plugins/blogspam.mdwn	2019-03-07 17:35:55.000000000 +1100
@@ -11,6 +11,8 @@
 go to your Preferences page, and click the "Comment Moderation" button.
 
 The plugin requires the [[!cpan JSON]] perl module.
+The [[!cpan LWPx::ParanoidAgent]] Perl module is recommended,
+although this plugin can also fall back to [[!cpan LWP]].
 
 You can control how content is tested via the `blogspam_options` setting.
 The list of options is [here](http://blogspam.net/api/testComment.html#options).
diff -Nru ikiwiki-3.20141016.4/doc/plugins/openid.mdwn ikiwiki-3.20141016.4+deb8u1/doc/plugins/openid.mdwn
--- ikiwiki-3.20141016.4/doc/plugins/openid.mdwn	2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/doc/plugins/openid.mdwn	2019-03-07 17:35:55.000000000 +1100
@@ -7,8 +7,11 @@
 The plugin needs the [[!cpan Net::OpenID::Consumer]] perl module.
 Version 1.x is needed in order for OpenID v2 to work.
 
-The [[!cpan LWPx::ParanoidAgent]] perl module is used if available, for
-added security. Finally, the [[!cpan Crypt::SSLeay]] perl module is needed
+The [[!cpan LWPx::ParanoidAgent]] Perl module is strongly recommended.
+The [[!cpan LWP]] module can also be used, but is susceptible to
+server-side request forgery.
+
+The [[!cpan Crypt::SSLeay]] Perl module is needed
 to support users entering "https" OpenID urls.
 
 This plugin is enabled by default, but can be turned off if you want to
diff -Nru ikiwiki-3.20141016.4/doc/plugins/pinger.mdwn ikiwiki-3.20141016.4+deb8u1/doc/plugins/pinger.mdwn
--- ikiwiki-3.20141016.4/doc/plugins/pinger.mdwn	2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/doc/plugins/pinger.mdwn	2019-03-07 17:35:55.000000000 +1100
@@ -10,9 +10,11 @@
 To configure what URLs to ping, use the [[ikiwiki/directive/ping]]
 [[ikiwiki/directive]].
 
-The [[!cpan LWP]] perl module is used for pinging. Or the [[!cpan
-LWPx::ParanoidAgent]] perl module is used if available, for added security.
-Finally, the [[!cpan Crypt::SSLeay]] perl module is needed to support pinging
+The [[!cpan LWPx::ParanoidAgent]] Perl module is strongly recommended.
+The [[!cpan LWP]] module can also be used, but is susceptible
+to server-side request forgery.
+
+The [[!cpan Crypt::SSLeay]] perl module is needed to support pinging
 "https" urls.
 
 By default the pinger will try to ping a site for 15 seconds before timing
diff -Nru ikiwiki-3.20141016.4/doc/security.mdwn ikiwiki-3.20141016.4+deb8u1/doc/security.mdwn
--- ikiwiki-3.20141016.4/doc/security.mdwn	2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/doc/security.mdwn	2019-03-07 17:35:55.000000000 +1100
@@ -526,3 +526,52 @@
 able to attach images. Upgrading ImageMagick to a version where
 CVE-2016-3714 has been fixed is also recommended, but at the time of
 writing no such version is available.
+
+## Server-side request forgery via aggregate plugin
+
+The ikiwiki maintainers discovered that the [[plugins/aggregate]] plugin
+did not use [[!cpan LWPx::ParanoidAgent]]. On sites where the
+aggregate plugin is enabled, authorized wiki editors could tell ikiwiki
+to fetch potentially undesired URIs even if LWPx::ParanoidAgent was
+installed:
+
+* local files via `file:` URIs
+* other URI schemes that might be misused by attackers, such as `gopher:`
+* hosts that resolve to loopback IP addresses (127.x.x.x)
+* hosts that resolve to RFC 1918 IP addresses (192.168.x.x etc.)
+
+This could be used by an attacker to publish information that should not have
+been accessible, cause denial of service by requesting "tarpit" URIs that are
+slow to respond, or cause undesired side-effects if local web servers implement
+["unsafe"](https://tools.ietf.org/html/rfc7231#section-4.2.1) GET requests.
+([[!debcve CVE-2019-9187]])
+
+Additionally, if the LWPx::ParanoidAgent module was not installed, the
+[[plugins/blogspam]], [[plugins/openid]] and [[plugins/pinger]] plugins
+would fall back to [[!cpan LWP]], which is susceptible to similar attacks.
+This is unlikely to be a practical problem for the blogspam plugin because
+the URL it requests is under the control of the wiki administrator, but
+the openid plugin can request URLs controlled by unauthenticated remote
+users, and the pinger plugin can request URLs controlled by authorized
+wiki editors.
+
+This is addressed in ikiwiki 3.20190228 as follows, with the same fixes
+backported to Debian 9 in version 3.20170111.1:
+
+* URI schemes other than `http:` and `https:` are not accepted, preventing
+  access to `file:`, `gopher:`, etc.
+
+* If a proxy is [[configured in the ikiwiki setup file|tips/using_a_proxy]],
+  it is used for all outgoing `http:` and `https:` requests. In this case
+  the proxy is responsible for blocking any requests that are undesired,
+  including loopback or RFC 1918 addresses.
+
+* If a proxy is not configured, and LWPx::ParanoidAgent is installed,
+  it will be used. This prevents loopback and RFC 1918 IP addresses, and
+  sets a timeout to avoid denial of service via "tarpit" URIs.
+
+* Otherwise, the ordinary LWP user-agent will be used. This allows requests
+  to loopback and RFC 1918 IP addresses, and has less robust timeout
+  behaviour. We are not treating this as a vulnerability: if this
+  behaviour is not acceptable for your site, please make sure to install
+  LWPx::ParanoidAgent or disable the affected plugins.
diff -Nru ikiwiki-3.20141016.4/doc/tips/using_a_proxy.mdwn ikiwiki-3.20141016.4+deb8u1/doc/tips/using_a_proxy.mdwn
--- ikiwiki-3.20141016.4/doc/tips/using_a_proxy.mdwn	1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/doc/tips/using_a_proxy.mdwn	2019-03-07 17:35:55.000000000 +1100
@@ -0,0 +1,22 @@
+Some ikiwiki plugins make outgoing HTTP requests from the web server:
+
+* [[plugins/aggregate]] (to download Atom and RSS feeds)
+* [[plugins/blogspam]] (to check whether a comment or edit is spam)
+* [[plugins/openid]] (to authenticate users)
+* [[plugins/pinger]] (to ping other ikiwiki installations)
+
+If your ikiwiki installation cannot contact the Internet without going
+through a proxy, you can configure this in the [[setup file|setup]] by
+setting environment variables:
+
+    ENV:
+        http_proxy: "http://proxy.example.com:8080";
+        https_proxy: "http://proxy.example.com:8080";
+        # optional
+        no_proxy: ".example.com,www.example.org"
+
+Note that some plugins will use the configured proxy for all destinations,
+even if they are listed in `no_proxy`.
+
+To avoid server-side request forgery attacks, ensure that your proxy does
+not allow requests to addresses that are considered to be internal.
diff -Nru ikiwiki-3.20141016.4/IkiWiki/Plugin/aggregate.pm ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/aggregate.pm
--- ikiwiki-3.20141016.4/IkiWiki/Plugin/aggregate.pm	2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/aggregate.pm	2019-03-07 17:35:55.000000000 +1100
@@ -513,7 +513,10 @@
 			}
 			$feed->{feedurl}=pop @urls;
 		}
-		my $ua=useragent();
+		# Using the for_url parameter makes sure we crash if used
+		# with an older IkiWiki.pm that didn't automatically try
+		# to use LWPx::ParanoidAgent.
+		my $ua=useragent(for_url => $feed->{feedurl});
 		my $res=URI::Fetch->fetch($feed->{feedurl}, UserAgent=>$ua);
 		if (! $res) {
 			$feed->{message}=URI::Fetch->errstr;
diff -Nru ikiwiki-3.20141016.4/IkiWiki/Plugin/blogspam.pm ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/blogspam.pm
--- ikiwiki-3.20141016.4/IkiWiki/Plugin/blogspam.pm	2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/blogspam.pm	2019-03-07 17:35:55.000000000 +1100
@@ -57,18 +57,10 @@
 	};
 	error $@ if $@;
 
-	eval q{use LWPx::ParanoidAgent};
-	if (!$@) {
-		$client=LWPx::ParanoidAgent->new(agent => $config{useragent});
-	}
-	else {
-		eval q{use LWP};
-		if ($@) {
-			error $@;
-			return;
-		}
-		$client=useragent();
-	}
+	# Using the for_url parameter makes sure we crash if used
+	# with an older IkiWiki.pm that didn't automatically try
+	# to use LWPx::ParanoidAgent.
+	$client=useragent(for_url => $config{blogspam_server});
 }
 
 sub checkcontent (@) {
diff -Nru ikiwiki-3.20141016.4/IkiWiki/Plugin/openid.pm ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/openid.pm
--- ikiwiki-3.20141016.4/IkiWiki/Plugin/openid.pm	2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/openid.pm	2019-03-07 17:35:55.000000000 +1100
@@ -237,14 +237,10 @@
 	eval q{use Net::OpenID::Consumer};
 	error($@) if $@;
 
-	my $ua;
-	eval q{use LWPx::ParanoidAgent};
-	if (! $@) {
-		$ua=LWPx::ParanoidAgent->new(agent => $config{useragent});
-	}
-	else {
-		$ua=useragent();
-	}
+	# We pass the for_url parameter, even though it's undef, because
+	# that will make sure we crash if used with an older IkiWiki.pm
+	# that didn't automatically try to use LWPx::ParanoidAgent.
+	my $ua=useragent(for_url => undef);
 
 	# Store the secret in the session.
 	my $secret=$session->param("openid_secret");
diff -Nru ikiwiki-3.20141016.4/IkiWiki/Plugin/pinger.pm ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/pinger.pm
--- ikiwiki-3.20141016.4/IkiWiki/Plugin/pinger.pm	2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/pinger.pm	2019-03-07 17:35:55.000000000 +1100
@@ -70,17 +70,16 @@
 		eval q{use Net::INET6Glue::INET_is_INET6}; # may not be available
 		
 		my $ua;
-		eval q{use LWPx::ParanoidAgent};
-		if (!$@) {
-			$ua=LWPx::ParanoidAgent->new(agent => $config{useragent});
-		}
-		else {
-			eval q{use LWP};
-			if ($@) {
-				debug(gettext("LWP not found, not pinging"));
-				return;
-			}
-			$ua=useragent();
+		eval {
+			# We pass the for_url parameter, even though it's
+			# undef, because that will make sure we crash if used
+			# with an older IkiWiki.pm that didn't automatically
+			# try to use LWPx::ParanoidAgent.
+			$ua=useragent(for_url => undef);
+		};
+		if ($@) {
+			debug(gettext("LWP not found, not pinging").": $@");
+			return;
 		}
 		$ua->timeout($config{pinger_timeout} || 15);
 		
diff -Nru ikiwiki-3.20141016.4/IkiWiki.pm ikiwiki-3.20141016.4+deb8u1/IkiWiki.pm
--- ikiwiki-3.20141016.4/IkiWiki.pm	2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/IkiWiki.pm	2019-03-07 17:35:55.000000000 +1100
@@ -2367,12 +2367,131 @@
 	$autofiles{$file}{generator}=$generator;
 }
 
-sub useragent () {
-	return LWP::UserAgent->new(
-		cookie_jar => $config{cookiejar},
-		env_proxy => 1,		# respect proxy env vars
+sub useragent (@) {
+	my %params = @_;
+	my $for_url = delete $params{for_url};
+	# Fail safe, in case a plugin calling this function is relying on
+	# a future parameter to make the UA more strict
+	foreach my $key (keys %params) {
+		error "Internal error: useragent(\"$key\" => ...) not understood";
+	}
+
+	eval q{use LWP};
+	error($@) if $@;
+
+	my %args = (
 		agent => $config{useragent},
+		cookie_jar => $config{cookiejar},
+		env_proxy => 0,
+		protocols_allowed => [qw(http https)],
 	);
+	my %proxies;
+
+	if (defined $for_url) {
+		# We know which URL we're going to fetch, so we can choose
+		# whether it's going to go through a proxy or not.
+		#
+		# We reimplement http_proxy, https_proxy and no_proxy here, so
+		# that we are not relying on LWP implementing them exactly the
+		# same way we do.
+
+		eval q{use URI};
+		error($@) if $@;
+
+		my $proxy;
+		my $uri = URI->new($for_url);
+
+		if ($uri->scheme eq 'http') {
+			$proxy = $ENV{http_proxy};
+			# HTTP_PROXY is deliberately not implemented
+			# because the HTTP_* namespace is also used by CGI
+		}
+		elsif ($uri->scheme eq 'https') {
+			$proxy = $ENV{https_proxy};
+			$proxy = $ENV{HTTPS_PROXY} unless defined $proxy;
+		}
+		else {
+			$proxy = undef;
+		}
+
+		foreach my $var (qw(no_proxy NO_PROXY)) {
+			my $no_proxy = $ENV{$var};
+			if (defined $no_proxy) {
+				foreach my $domain (split /\s*,\s*/, $no_proxy) {
+					if ($domain =~ s/^\*?\.//) {
+						# no_proxy="*.example.com" or
+						# ".example.com": match suffix
+						# against .example.com
+						if ($uri->host =~ m/(^|\.)\Q$domain\E$/i) {
+							$proxy = undef;
+						}
+					}
+					else {
+						# no_proxy="example.com":
+						# match exactly example.com
+						if (lc $uri->host eq lc $domain) {
+							$proxy = undef;
+						}
+					}
+				}
+			}
+		}
+
+		if (defined $proxy) {
+			$proxies{$uri->scheme} = $proxy;
+			# Paranoia: make sure we can't bypass the proxy
+			$args{protocols_allowed} = [$uri->scheme];
+		}
+	}
+	else {
+		# The plugin doesn't know yet which URL(s) it's going to
+		# fetch, so we have to make some conservative assumptions.
+		my $http_proxy = $ENV{http_proxy};
+		my $https_proxy = $ENV{https_proxy};
+		$https_proxy = $ENV{HTTPS_PROXY} unless defined $https_proxy;
+
+		# We don't respect no_proxy here: if we are not using the
+		# paranoid user-agent, then we need to give the proxy the
+		# opportunity to reject undesirable requests.
+
+		# If we have one, we need the other: otherwise, neither
+		# LWPx::ParanoidAgent nor the proxy would have the
+		# opportunity to filter requests for the other protocol.
+		if (defined $https_proxy && defined $http_proxy) {
+			%proxies = (http => $http_proxy, https => $https_proxy);
+		}
+		elsif (defined $https_proxy) {
+			%proxies = (http => $https_proxy, https => $https_proxy);
+		}
+		elsif (defined $http_proxy) {
+			%proxies = (http => $http_proxy, https => $http_proxy);
+		}
+
+	}
+
+	if (scalar keys %proxies) {
+		# The configured proxy is responsible for deciding which
+		# URLs are acceptable to fetch and which URLs are not.
+		my $ua = LWP::UserAgent->new(%args);
+		foreach my $scheme (@{$ua->protocols_allowed}) {
+			unless ($proxies{$scheme}) {
+				error "internal error: $scheme is allowed but has no proxy";
+			}
+		}
+		# We can't pass the proxies in %args because that only
+		# works since LWP 6.24.
+		foreach my $scheme (keys %proxies) {
+			$ua->proxy($scheme, $proxies{$scheme});
+		}
+		return $ua;
+	}
+
+	eval q{use LWPx::ParanoidAgent};
+	if ($@) {
+		print STDERR "warning: installing LWPx::ParanoidAgent is recommended\n";
+		return LWP::UserAgent->new(%args);
+	}
+	return LWPx::ParanoidAgent->new(%args);
 }
 
 sub sortspec_translate ($$) {
diff -Nru ikiwiki-3.20141016.4/t/aggregate-file.t ikiwiki-3.20141016.4+deb8u1/t/aggregate-file.t
--- ikiwiki-3.20141016.4/t/aggregate-file.t	1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/t/aggregate-file.t	2019-03-07 17:35:55.000000000 +1100
@@ -0,0 +1,173 @@
+#!/usr/bin/perl
+use utf8;
+use warnings;
+use strict;
+
+use Encode;
+use Test::More;
+
+BEGIN {
+	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 --plugin inline);
+}
+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 %params = @_;
+	my %setup = (
+		wikiname => 'this is the name of my wiki',
+		srcdir => getcwd.'/t/tmp/in',
+		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(aggregate)],
+		disable_plugins => [qw(emailauth openid passwordauth)],
+		aggregate_webtrigger => 1,
+	);
+	if ($params{without_paranoia}) {
+		$setup{libdirs} = [getcwd.'/t/noparanoia'];
+	}
+	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(! system(@command, qw(--setup t/tmp/test.setup --rebuild --wrappers)));
+}
+
+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();
+	diag $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 decode_utf8($out);
+}
+
+sub test {
+	my $content;
+
+	ok(! system(qw(rm -rf t/tmp)));
+	ok(! system(qw(mkdir t/tmp)));
+
+	write_old_file('aggregator.mdwn', 't/tmp/in',
+		'[[!aggregate name="ssrf" url="file://'.getcwd.'/t/secret.rss"]]'
+		.'[[!inline pages="internal(aggregator/*)"]]');
+
+	write_setup_file();
+	thoroughly_rebuild();
+
+	$content = run_cgi(
+		method => 'GET',
+		params => {
+			do => 'aggregate_webtrigger',
+		},
+	);
+	unlike($content, qr{creating new page});
+	unlike($content, qr{Secrets});
+	ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf');
+	ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf/Secrets_go_here._aggregated');
+
+	thoroughly_rebuild();
+	$content = readfile('t/tmp/out/aggregator/index.html');
+	unlike($content, qr{Secrets});
+
+	diag('Trying test again with LWPx::ParanoidAgent disabled');
+
+	write_setup_file(without_paranoia => 1);
+	thoroughly_rebuild();
+
+	$content = run_cgi(
+		method => 'GET',
+		params => {
+			do => 'aggregate_webtrigger',
+		},
+	);
+	unlike($content, qr{creating new page});
+	unlike($content, qr{Secrets});
+	ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf');
+	ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf/Secrets_go_here._aggregated');
+
+	thoroughly_rebuild();
+	$content = readfile('t/tmp/out/aggregator/index.html');
+	unlike($content, qr{Secrets});
+}
+
+test();
+
+done_testing();
diff -Nru ikiwiki-3.20141016.4/t/noparanoia/LWPx/ParanoidAgent.pm ikiwiki-3.20141016.4+deb8u1/t/noparanoia/LWPx/ParanoidAgent.pm
--- ikiwiki-3.20141016.4/t/noparanoia/LWPx/ParanoidAgent.pm	1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/t/noparanoia/LWPx/ParanoidAgent.pm	2019-03-07 17:35:55.000000000 +1100
@@ -0,0 +1,2 @@
+# make import fail
+0;
diff -Nru ikiwiki-3.20141016.4/t/secret.rss ikiwiki-3.20141016.4+deb8u1/t/secret.rss
--- ikiwiki-3.20141016.4/t/secret.rss	1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/t/secret.rss	2019-03-07 17:35:55.000000000 +1100
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<rss version="2.0">
+<channel>
+<title>Secrets go here</title>
+<description>Secrets go here</description>
+<item>
+  <title>Secrets go here</title>
+  <description>Secrets go here</description>
+</item>
+</channel>
+</rss>
diff -Nru ikiwiki-3.20141016.4/t/useragent.t ikiwiki-3.20141016.4+deb8u1/t/useragent.t
--- ikiwiki-3.20141016.4/t/useragent.t	1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/t/useragent.t	2019-03-07 17:35:55.000000000 +1100
@@ -0,0 +1,317 @@
+#!/usr/bin/perl
+use warnings;
+use strict;
+use Test::More;
+
+my $have_paranoid_agent;
+BEGIN {
+	plan(skip_all => 'LWP not available')
+		unless eval q{
+			use LWP qw(); 1;
+		};
+	use_ok("IkiWiki");
+	$have_paranoid_agent = eval q{
+		use LWPx::ParanoidAgent qw(); 1;
+	};
+}
+
+eval { useragent(future_feature => 1); };
+ok($@, 'future features should cause useragent to fail');
+
+diag "==== No proxy ====";
+delete $ENV{http_proxy};
+delete $ENV{https_proxy};
+delete $ENV{no_proxy};
+delete $ENV{HTTPS_PROXY};
+delete $ENV{NO_PROXY};
+
+diag "---- Unspecified URL ----";
+my $ua = useragent(for_url => undef);
+SKIP: {
+	skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+	ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef, 'No http proxy');
+is($ua->proxy('https'), undef, 'No https proxy');
+
+diag "---- Specified URL ----";
+$ua = useragent(for_url => 'http://example.com');
+SKIP: {
+	skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+	ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef, 'No http proxy');
+is($ua->proxy('https'), undef, 'No https proxy');
+
+diag "==== Proxy for everything ====";
+$ENV{http_proxy} = 'http://proxy:8080';
+$ENV{https_proxy} = 'http://sproxy:8080';
+delete $ENV{no_proxy};
+delete $ENV{HTTPS_PROXY};
+delete $ENV{NO_PROXY};
+
+diag "---- Unspecified URL ----";
+$ua = useragent(for_url => undef);
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
+$ua = useragent(for_url => 'http://example.com');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+# We don't care what $ua->proxy('https') is, because it won't be used
+$ua = useragent(for_url => 'https://example.com');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
+# We don't care what $ua->proxy('http') is, because it won't be used
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
+
+diag "==== Selective proxy ====";
+$ENV{http_proxy} = 'http://proxy:8080';
+$ENV{https_proxy} = 'http://sproxy:8080';
+$ENV{no_proxy} = '*.example.net,example.com,.example.org';
+delete $ENV{HTTPS_PROXY};
+delete $ENV{NO_PROXY};
+
+diag "---- Unspecified URL ----";
+$ua = useragent(for_url => undef);
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
+
+diag "---- Exact match for no_proxy ----";
+$ua = useragent(for_url => 'http://example.com');
+SKIP: {
+	skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+	ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- Subdomain of exact domain in no_proxy ----";
+$ua = useragent(for_url => 'http://sub.example.com');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+
+diag "---- example.net matches *.example.net ----";
+$ua = useragent(for_url => 'https://example.net');
+SKIP: {
+	skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+	ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- sub.example.net matches *.example.net ----";
+$ua = useragent(for_url => 'https://sub.example.net');
+SKIP: {
+	skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+	ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- badexample.net does not match *.example.net ----";
+$ua = useragent(for_url => 'https://badexample.net');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
+
+diag "---- example.org matches .example.org ----";
+$ua = useragent(for_url => 'https://example.org');
+SKIP: {
+	skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+	ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- sub.example.org matches .example.org ----";
+$ua = useragent(for_url => 'https://sub.example.org');
+SKIP: {
+	skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+	ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- badexample.org does not match .example.org ----";
+$ua = useragent(for_url => 'https://badexample.org');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
+
+diag "==== Selective proxy (alternate variables) ====";
+$ENV{http_proxy} = 'http://proxy:8080';
+delete $ENV{https_proxy};
+$ENV{HTTPS_PROXY} = 'http://sproxy:8080';
+delete $ENV{no_proxy};
+$ENV{NO_PROXY} = '*.example.net,example.com,.example.org';
+
+diag "---- Unspecified URL ----";
+$ua = useragent(for_url => undef);
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
+
+diag "---- Exact match for no_proxy ----";
+$ua = useragent(for_url => 'http://example.com');
+SKIP: {
+	skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+	ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- Subdomain of exact domain in no_proxy ----";
+$ua = useragent(for_url => 'http://sub.example.com');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+
+diag "---- example.net matches *.example.net ----";
+$ua = useragent(for_url => 'https://example.net');
+SKIP: {
+	skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+	ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- sub.example.net matches *.example.net ----";
+$ua = useragent(for_url => 'https://sub.example.net');
+SKIP: {
+	skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+	ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- badexample.net does not match *.example.net ----";
+$ua = useragent(for_url => 'https://badexample.net');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
+
+diag "---- example.org matches .example.org ----";
+$ua = useragent(for_url => 'https://example.org');
+SKIP: {
+	skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+	ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- sub.example.org matches .example.org ----";
+$ua = useragent(for_url => 'https://sub.example.org');
+SKIP: {
+	skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+	ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- badexample.org does not match .example.org ----";
+$ua = useragent(for_url => 'https://badexample.org');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
+
+diag "==== Selective proxy (many variables) ====";
+$ENV{http_proxy} = 'http://proxy:8080';
+$ENV{https_proxy} = 'http://sproxy:8080';
+# This one should be ignored in favour of https_proxy
+$ENV{HTTPS_PROXY} = 'http://not.preferred.proxy:3128';
+# These two should be merged
+$ENV{no_proxy} = '*.example.net,example.com';
+$ENV{NO_PROXY} = '.example.org';
+
+diag "---- Unspecified URL ----";
+$ua = useragent(for_url => undef);
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
+
+diag "---- Exact match for no_proxy ----";
+$ua = useragent(for_url => 'http://example.com');
+SKIP: {
+	skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+	ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- Subdomain of exact domain in no_proxy ----";
+$ua = useragent(for_url => 'http://sub.example.com');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+
+diag "---- example.net matches *.example.net ----";
+$ua = useragent(for_url => 'https://example.net');
+SKIP: {
+	skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+	ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- sub.example.net matches *.example.net ----";
+$ua = useragent(for_url => 'https://sub.example.net');
+SKIP: {
+	skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+	ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- badexample.net does not match *.example.net ----";
+$ua = useragent(for_url => 'https://badexample.net');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
+
+diag "==== One but not the other ====\n";
+$ENV{http_proxy} = 'http://proxy:8080';
+delete $ENV{https_proxy};
+delete $ENV{HTTPS_PROXY};
+delete $ENV{no_proxy};
+delete $ENV{NO_PROXY};
+$ua = useragent(for_url => undef);
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+is($ua->proxy('https'), 'http://proxy:8080', 'should use proxy');
+
+delete $ENV{http_proxy};
+$ENV{https_proxy} = 'http://sproxy:8080';
+delete $ENV{HTTPS_PROXY};
+delete $ENV{no_proxy};
+delete $ENV{NO_PROXY};
+$ua = useragent(for_url => undef);
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), 'http://sproxy:8080', 'should use proxy');
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
+
+done_testing;

Reply to: