From a1f7858b7758d8ed3b9c8564ac242220604e4cf9 Mon Sep 17 00:00:00 2001
From: Brian Wolff <bawolff+wn@gmail.com>
Date: Sun, 7 Feb 2016 08:07:20 -0500
Subject: [PATCH] SECURITY: Do not directly redirect to interwikis, but use splash page

Directly redirecting based on a url paramter might potentially
be used in a phishing attack to confuse users.

Bug: T109140
Bug: T122209
Change-Id: I6c604439320fa876719933cc7f3a3ff04fb1a6ad
---
 autoload.php                                   |  1 +
 includes/OutputPage.php                        |  4 +-
 includes/Title.php                             | 27 +++++++++
 includes/specialpage/RedirectSpecialPage.php   |  2 +-
 includes/specialpage/SpecialPageFactory.php    |  1 +
 includes/specials/SpecialChangeCredentials.php |  2 +-
 includes/specials/SpecialChangeEmail.php       |  2 +-
 includes/specials/SpecialGoToInterwiki.php     | 79 ++++++++++++++++++++++++++
 includes/specials/SpecialPageLanguage.php      |  2 +-
 includes/specials/SpecialPreferences.php       |  2 +-
 includes/specials/SpecialSearch.php            |  2 +-
 includes/specials/helpers/LoginHelper.php      |  2 +-
 languages/i18n/en.json                         |  7 ++-
 languages/i18n/qqq.json                        |  7 ++-
 languages/messages/MessagesEn.php              |  1 +
 15 files changed, 131 insertions(+), 10 deletions(-)
 create mode 100644 includes/specials/SpecialGoToInterwiki.php

diff --git a/autoload.php b/autoload.php
index 7ed08df..a2aa1d0 100644
--- a/autoload.php
+++ b/autoload.php
@@ -1338,6 +1338,7 @@ $wgAutoloadLocalClasses = [
 	'SpecialExpandTemplates' => __DIR__ . '/includes/specials/SpecialExpandTemplates.php',
 	'SpecialExport' => __DIR__ . '/includes/specials/SpecialExport.php',
 	'SpecialFilepath' => __DIR__ . '/includes/specials/SpecialFilepath.php',
+	'SpecialGoToInterwiki' => __DIR__ . '/includes/specials/SpecialGoToInterwiki.php',
 	'SpecialImport' => __DIR__ . '/includes/specials/SpecialImport.php',
 	'SpecialJavaScriptTest' => __DIR__ . '/includes/specials/SpecialJavaScriptTest.php',
 	'SpecialLinkAccounts' => __DIR__ . '/includes/specials/SpecialLinkAccounts.php',
diff --git a/includes/OutputPage.php b/includes/OutputPage.php
index fd28f4a..5537434 100644
--- a/includes/OutputPage.php
+++ b/includes/OutputPage.php
@@ -2706,7 +2706,9 @@ class OutputPage extends ContextSource {
 		} else {
 			$titleObj = Title::newFromText( $returnto );
 		}
-		if ( !is_object( $titleObj ) ) {
+		// We don't want people to return to external interwiki. That
+		// might potentially be used as part of a phishing scheme
+		if ( !is_object( $titleObj ) || $titleObj->isExternal() ) {
 			$titleObj = Title::newMainPage();
 		}
 
diff --git a/includes/Title.php b/includes/Title.php
index 5cf911f..b163ed0 100644
--- a/includes/Title.php
+++ b/includes/Title.php
@@ -1682,6 +1682,33 @@ class Title implements LinkTarget {
 	}
 
 	/**
+	 * Get a url appropriate for making redirects based on an untrusted url arg
+	 *
+	 * This is basically the same as getFullUrl(), but in the case of external
+	 * interwikis, we send the user to a landing page, to prevent possible
+	 * phishing attacks and the like.
+	 *
+	 * @note Uses current protocol by default, since technically relative urls
+	 *   aren't allowed in redirects per HTTP spec, so this is not suitable for
+	 *   places where the url gets cached, as might pollute between
+	 *   https and non-https users.
+	 * @see self::getLocalURL for the arguments.
+	 * @param array|string $query
+	 * @param string $proto Protocol type to use in URL
+	 * @return String. A url suitable to use in an HTTP location header.
+	 */
+	public function getFullUrlForRedirect( $query = '', $proto = PROTO_CURRENT ) {
+		$target = $this;
+		if ( $this->isExternal() ) {
+			$target = SpecialPage::getTitleFor(
+				'GoToInterwiki',
+				$this->getPrefixedDBKey()
+			);
+		}
+		return $target->getFullUrl( $query, false, $proto );
+	}
+
+	/**
 	 * Get a URL with no fragment or server name (relative URL) from a Title object.
 	 * If this page is generated with action=render, however,
 	 * $wgServer is prepended to make an absolute URL.
diff --git a/includes/specialpage/RedirectSpecialPage.php b/includes/specialpage/RedirectSpecialPage.php
index ea7d783..01787d3 100644
--- a/includes/specialpage/RedirectSpecialPage.php
+++ b/includes/specialpage/RedirectSpecialPage.php
@@ -41,7 +41,7 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage {
 		$query = $this->getRedirectQuery();
 		// Redirect to a page title with possible query parameters
 		if ( $redirect instanceof Title ) {
-			$url = $redirect->getFullURL( $query );
+			$url = $redirect->getFullUrlForRedirect( $query );
 			$this->getOutput()->redirect( $url );
 
 			return $redirect;
diff --git a/includes/specialpage/SpecialPageFactory.php b/includes/specialpage/SpecialPageFactory.php
index daabded..d439e55 100644
--- a/includes/specialpage/SpecialPageFactory.php
+++ b/includes/specialpage/SpecialPageFactory.php
@@ -144,6 +144,7 @@ class SpecialPageFactory {
 		'RandomInCategory' => 'SpecialRandomInCategory',
 		'Randomredirect' => 'SpecialRandomredirect',
 		'Randomrootpage' => 'SpecialRandomrootpage',
+		'GoToInterwiki' => 'SpecialGoToInterwiki',
 
 		// High use pages
 		'Mostlinkedcategories' => 'MostlinkedCategoriesPage',
diff --git a/includes/specials/SpecialChangeCredentials.php b/includes/specials/SpecialChangeCredentials.php
index b81ca3d..45af71f 100644
--- a/includes/specials/SpecialChangeCredentials.php
+++ b/includes/specials/SpecialChangeCredentials.php
@@ -238,7 +238,7 @@ class SpecialChangeCredentials extends AuthManagerSpecialPage {
 		}
 
 		$title = Title::newFromText( $returnTo );
-		return $title->getFullURL( $returnToQuery );
+		return $title->getFullUrlForRedirect( $returnToQuery );
 	}
 
 	protected function getRequestBlacklist() {
diff --git a/includes/specials/SpecialChangeEmail.php b/includes/specials/SpecialChangeEmail.php
index 785447f..eb98fe7 100644
--- a/includes/specials/SpecialChangeEmail.php
+++ b/includes/specials/SpecialChangeEmail.php
@@ -136,7 +136,7 @@ class SpecialChangeEmail extends FormSpecialPage {
 		$query = $request->getVal( 'returntoquery' );
 
 		if ( $this->status->value === true ) {
-			$this->getOutput()->redirect( $titleObj->getFullURL( $query ) );
+			$this->getOutput()->redirect( $titleObj->getFullUrlForRedirect( $query ) );
 		} elseif ( $this->status->value === 'eauth' ) {
 			# Notify user that a confirmation email has been sent...
 			$this->getOutput()->wrapWikiMsg( "<div class='error' style='clear: both;'>\n$1\n</div>",
diff --git a/includes/specials/SpecialGoToInterwiki.php b/includes/specials/SpecialGoToInterwiki.php
new file mode 100644
index 0000000..809a14a
--- /dev/null
+++ b/includes/specials/SpecialGoToInterwiki.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Implements Special:GoToInterwiki
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Landing page for non-local interwiki links.
+ *
+ * Meant to warn people that the site they're visiting
+ * is not the local wiki (In case of phishing tricks).
+ * Only meant to be used for things that directly
+ * redirect from url (e.g. Special:Search/google:foo )
+ * Not meant for general interwiki linking (e.g.
+ * [[google:foo]] should still directly link)
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialGoToInterwiki extends UnlistedSpecialPage {
+	public function __construct( $name = 'GoToInterwiki' ) {
+		parent::__construct( $name );
+	}
+
+	public function execute( $par ) {
+		$this->setHeaders();
+		$target = Title::newFromText( $par );
+		// Disallow special pages as a precaution against
+		// possible redirect loops.
+		if ( !$target || $target->isSpecialPage() ) {
+			$this->getOutput()->setStatusCode( 404 );
+			$this->getOutput()->addWikiMsg( 'gotointerwiki-invalid' );
+			return;
+		}
+
+		$url = $target->getFullURL();
+		if ( !$target->isExternal() || $target->isLocal() ) {
+			// Either a normal page, or a local interwiki.
+			// just redirect.
+			$this->getOutput()->redirect( $url, '301' );
+		} else {
+			$this->getOutput()->addWikiMsg(
+				'gotointerwiki-external',
+				$url,
+				$target->getFullText()
+			);
+		}
+	}
+
+	/**
+	 * @return bool
+	 */
+	public function requiresWrite() {
+		return false;
+	}
+
+	/**
+	 * @return String
+	 */
+	protected function getGroupName() {
+		return 'redirects';
+	}
+}
diff --git a/includes/specials/SpecialPageLanguage.php b/includes/specials/SpecialPageLanguage.php
index db05ebe..2943fd4 100644
--- a/includes/specials/SpecialPageLanguage.php
+++ b/includes/specials/SpecialPageLanguage.php
@@ -136,7 +136,7 @@ class SpecialPageLanguage extends FormSpecialPage {
 		}
 
 		// Url to redirect to after the operation
-		$this->goToUrl = $title->getFullURL(
+		$this->goToUrl = $title->getFullUrlForRedirect(
 			$title->isRedirect() ? [ 'redirect' => 'no' ] : []
 		);
 
diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php
index eee5b64..40b50ea 100644
--- a/includes/specials/SpecialPreferences.php
+++ b/includes/specials/SpecialPreferences.php
@@ -148,7 +148,7 @@ class SpecialPreferences extends SpecialPage {
 		// Set session data for the success message
 		$this->getRequest()->getSession()->set( 'specialPreferencesSaveSuccess', 1 );
 
-		$url = $this->getPageTitle()->getFullURL();
+		$url = $this->getPageTitle()->getFullUrlForRedirect();
 		$this->getOutput()->redirect( $url );
 
 		return true;
diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php
index 34620ff..13519eb 100644
--- a/includes/specials/SpecialSearch.php
+++ b/includes/specials/SpecialSearch.php
@@ -251,7 +251,7 @@ class SpecialSearch extends SpecialPage {
 			return null;
 		}
 
-		return $url === null ? $title->getFullURL() : $url;
+		return $url === null ? $title->getFullUrlForRedirect() : $url;
 	}
 
 	/**
diff --git a/includes/specials/helpers/LoginHelper.php b/includes/specials/helpers/LoginHelper.php
index f853f41..cfcbf65 100644
--- a/includes/specials/helpers/LoginHelper.php
+++ b/includes/specials/helpers/LoginHelper.php
@@ -89,7 +89,7 @@ class LoginHelper extends ContextSource {
 		}
 
 		if ( $type === 'successredirect' ) {
-			$redirectUrl = $returnToTitle->getFullURL( $returnToQuery, false, $proto );
+			$redirectUrl = $returnToTitle->getFullUrlForRedirect( $returnToQuery, $proto );
 			$this->getOutput()->redirect( $redirectUrl );
 		} else {
 			$this->getOutput()->addReturnTo( $returnToTitle, $returnToQuery, null, $options );
diff --git a/languages/i18n/en.json b/languages/i18n/en.json
index f2ff14b..070754d 100644
--- a/languages/i18n/en.json
+++ b/languages/i18n/en.json
@@ -4258,5 +4258,10 @@
 	"restrictionsfield-label": "Allowed IP ranges:",
 	"restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use:<pre>0.0.0.0/0\n::/0</pre>",
 	"revid": "revision $1",
-	"pageid": "page ID $1"
+	"pageid": "page ID $1",
+	"edit-error-short": "Error: $1",
+	"edit-error-long": "Errors:\n\n$1",
+	"gotointerwiki": "Leaving {{SITENAME}}",
+	"gotointerwiki-invalid": "The specified title was invalid.",
+	"gotointerwiki-external": "You are about to leave {{SITENAME}} to visit [[$2]] which is a separate website.\n\n[$1 Click here to continue on to $1]."
 }
diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json
index 1e71b88..8592b77 100644
--- a/languages/i18n/qqq.json
+++ b/languages/i18n/qqq.json
@@ -4444,5 +4444,10 @@
 	"restrictionsfield-label": "Field label shown for restriction fields (e.g. on Special:BotPassword).",
 	"restrictionsfield-help": "Placeholder text displayed in restriction fields (e.g. on Special:BotPassword).",
 	"revid": "Used to format a revision ID number in text. Parameters:\n* $1 - Revision ID number.\n{{Identical|Revision}}",
-	"pageid": "Used to format a page ID number in text. Parameters:\n* $1 - Page ID number."
+	"pageid": "Used to format a page ID number in text. Parameters:\n* $1 - Page ID number.",
+	"edit-error-short": "Error message. Parameters:\n* $1 - the error details\nSee also:\n* {{msg-mw|edit-error-long}}\n{{Identical|Error}}",
+	"edit-error-long": "Error message. Parameters:\n* $1 - the error details\nSee also:\n* {{msg-mw|edit-error-short}}\n{{Identical|Error}}",
+	"gotointerwiki": "{{doc-special|GoToInterwiki}}\n\nSpecial:GoToInterwiki is a warning page displayed before redirecting users to external interwiki links. Its triggered by people going to something like [[Special:Search/google:foo]].",
+	"gotointerwiki-invalid": "Message shown on Special:GoToInterwiki if given an invalid title.",
+	"gotointerwiki-external": "Message shown on Special:GoToInterwiki if given a external interwiki link (e.g. [[Special:GoToInterwiki/Google:Foo]]). $1 is the full url the user is trying to get to. $2 is the text of the interwiki link (e.g. \"Google:foo\")."
 }
diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php
index 689586b9..7fceb6c 100644
--- a/languages/messages/MessagesEn.php
+++ b/languages/messages/MessagesEn.php
@@ -427,6 +427,7 @@ $specialPageAliases = [
 	'Fewestrevisions'           => [ 'FewestRevisions' ],
 	'FileDuplicateSearch'       => [ 'FileDuplicateSearch' ],
 	'Filepath'                  => [ 'FilePath' ],
+	'GoToInterwiki'             => [ 'GoToInterwiki' ],
 	'Import'                    => [ 'Import' ],
 	'Invalidateemail'           => [ 'InvalidateEmail' ],
 	'JavaScriptTest'            => [ 'JavaScriptTest' ],
-- 
2.1.4

