CVE-2021-39186: Improper Input Validation (CWE-20)
value: GlobalNewFiles is a MediaWiki extension maintained by Miraheze. Prior to commit number cee254e1b158cdb0ddbea716b1d3edc31fa4fb5d, the username column of the GlobalNewFiles special page is vulnerable to a stored XSS. Commit number cee254e1b158cdb0ddbea716b1d3edc31fa4fb5d contains a patch. As a workaround, one may disallow <,> (or other characters required to insert html/js) from being used in account names so an XSS is not possible.
CWE description: The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
CVSS v3 base score: 6.1
Published: 2021-09-01T21:15Z
Repository: https://github.com/miraheze/GlobalNewFiles
Language: PHP
Files changed: extension.json, includes/GlobalNewFilesHooks.php, includes/GlobalNewFilesPager.php, includes/SpecialGlobalNewFiles.php
Commit date: 2021-09-01 19:59:49 +0300
Commit message: SECURITY: Fix XSS issues GlobalNewFilesPager

Fix XSS issues in GlobalNewFilesPager by using MediaWiki's LinkRenderer
and Html utilities instead of constructing (unescaped) HTML messages
directly.

For more details, see https://phabricator.miraheze.org/T7935.

Diff: diff --git a/extension.json b/extension.json
index f940a35..4ea9b5c 100644
--- a/extension.json
+++ b/extension.json
@@ -39,11 +39,16 @@
 		"GlobalNewFilesInsertJob": "GlobalNewFilesInsertJob",
 		"GlobalNewFilesDeleteJob": "GlobalNewFilesDeleteJob",
 		"GlobalNewFilesMoveJob": "GlobalNewFilesMoveJob"
 	},
 	"SpecialPages": {
-		"GlobalNewFiles": "SpecialGlobalNewFiles"
+		"GlobalNewFiles": {
+			"class": "SpecialGlobalNewFiles",
+			"services": [
+				"LinkRenderer"
+			]
+		}
 	},
 	"Hooks": {
 		"CreateWikiTables": [
 			"GlobalNewFilesHooks::onCreateWikiTables"
 		],
diff --git a/includes/GlobalNewFilesHooks.php b/includes/GlobalNewFilesHooks.php
index 51f5315..fed9dab 100644
--- a/includes/GlobalNewFilesHooks.php
+++ b/includes/GlobalNewFilesHooks.php
@@ -40,11 +40,11 @@ class GlobalNewFilesHooks {
 			);
 
 			$updater->modifyExtensionField(
 				'gnf_files',
 				'files_timestamp',
-				__DIR__ . '/../sql/patches/patch-gnf_files-binary.sql' 
+				__DIR__ . '/../sql/patches/patch-gnf_files-binary.sql'
 			);
 
 			$updater->modifyTable(
  				'gnf_files',
   				__DIR__ . '/../sql/patches/patch-gnf_files-add-indexes.sql',
diff --git a/includes/GlobalNewFilesPager.php b/includes/GlobalNewFilesPager.php
index 7054852..0875a58 100644
--- a/includes/GlobalNewFilesPager.php
+++ b/includes/GlobalNewFilesPager.php
@@ -1,20 +1,26 @@
 <?php
 
+use MediaWiki\Linker\LinkRenderer;
 use MediaWiki\MediaWikiServices;
 
 class GlobalNewFilesPager extends TablePager {
-	function __construct() {
+	/** @var LinkRenderer */
+	private $linkRenderer;
+
+	function __construct( RequestContext $context, LinkRenderer $linkRenderer ) {
+		parent::__construct( $context );
+
+		$this->linkRenderer = $linkRenderer;
+
 		$this->mDb = GlobalNewFilesHooks::getGlobalDB( DB_REPLICA, 'gnf_files' );
 
-		if ( $this->getRequest()->getText( 'sort', 'files_date' ) == 'files_date' ) {
+		if ( $context->getRequest()->getText( 'sort', 'files_date' ) == 'files_date' ) {
 			$this->mDefaultDirection = IndexPager::DIR_DESCENDING;
 		} else {
 			$this->mDefaultDirection = IndexPager::DIR_ASCENDING;
 		}
-
-		parent::__construct( $this->getContext() );
 	}
 
 	function getFieldNames() {
 		static $headers = null;
 
@@ -34,27 +40,41 @@ class GlobalNewFilesPager extends TablePager {
 	}
 
 	function formatValue( $name, $value ) {
 		$row = $this->mCurrentRow;
 
-		$wiki = $row->files_dbname;
-
 		switch ( $name ) {
 			case 'files_timestamp':
 				$formatted = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $row->files_timestamp, $this->getUser() ) );
 				break;
 			case 'files_dbname':
 				$formatted = $row->files_dbname;
 				break;
 			case 'files_url':
-				$formatted = "<img src=\"{$row->files_url}\" style=\"width:135px;height:135px;\">";
+				$formatted = Html::element(
+					'img',
+					[
+						'src' => $row->files_url,
+						'style' => 'width: 135px; height: 135px;'
+					]
+				);
 				break;
 			case 'files_name':
-				$formatted = "<a href=\"{$row->files_page}\">{$row->files_name}</a>";
+				$formatted = Html::element(
+					'a',
+					[
+						'href' => $row->files_page,
+					],
+					$row->files_name
+				);
+
 				break;
 			case 'files_user':
-				$formatted = "<a href=\"/wiki/Special:CentralAuth/{$row->files_user}\">{$row->files_user}</a>";
+				$formatted = $this->linkRenderer->makeLink(
+					SpecialPage::getTitleFor( 'CentralAuth', $row->files_user ),
+					$row->files_user
+				);
 				break;
 			default:
 				$formatted = "Unable to format $name";
 				break;
 		}
diff --git a/includes/SpecialGlobalNewFiles.php b/includes/SpecialGlobalNewFiles.php
index ec36b9d..7b18d75 100644
--- a/includes/SpecialGlobalNewFiles.php
+++ b/includes/SpecialGlobalNewFiles.php
@@ -1,19 +1,24 @@
 <?php
 
+use MediaWiki\Linker\LinkRenderer;
+
 class SpecialGlobalNewFiles extends SpecialPage {
+	/** @var LinkRenderer */
+	private $linkRenderer;
 
-	function __construct() {
+	function __construct( LinkRenderer $linkRenderer ) {
 		parent::__construct( 'GlobalNewFiles' );
+		$this->linkRenderer = $linkRenderer;
 	}
 
 	function execute( $par ) {
 		$this->setHeaders();
 		$this->outputHeader();
 
-		$pager = new GlobalNewFilesPager();
-		
+		$pager = new GlobalNewFilesPager( $this->getContext(), $this->linkRenderer );
+
 		$this->getOutput()->addParserOutputContent( $pager->getFullOutput() );
 	}
 
 	protected function getGroupName() {
 		return 'other';

Vulnerable code: 		"GlobalNewFiles": "SpecialGlobalNewFiles"
				__DIR__ . '/../sql/patches/patch-gnf_files-binary.sql' 
	function __construct() {
		if ( $this->getRequest()->getText( 'sort', 'files_date' ) == 'files_date' ) {

		parent::__construct( $this->getContext() );
		$wiki = $row->files_dbname;

				$formatted = "<img src=\"{$row->files_url}\" style=\"width:135px;height:135px;\">";
				$formatted = "<a href=\"{$row->files_page}\">{$row->files_name}</a>";
				$formatted = "<a href=\"/wiki/Special:CentralAuth/{$row->files_user}\">{$row->files_user}</a>";
	function __construct() {
		$pager = new GlobalNewFilesPager();
		
Fixed code: 		"GlobalNewFiles": {
			"class": "SpecialGlobalNewFiles",
			"services": [
				"LinkRenderer"
			]
		}
				__DIR__ . '/../sql/patches/patch-gnf_files-binary.sql'
use MediaWiki\Linker\LinkRenderer;
	/** @var LinkRenderer */
	private $linkRenderer;

	function __construct( RequestContext $context, LinkRenderer $linkRenderer ) {
		parent::__construct( $context );

		$this->linkRenderer = $linkRenderer;

		if ( $context->getRequest()->getText( 'sort', 'files_date' ) == 'files_date' ) {
				$formatted = Html::element(
					'img',
					[
						'src' => $row->files_url,
						'style' => 'width: 135px; height: 135px;'
					]
				);
				$formatted = Html::element(
					'a',
					[
						'href' => $row->files_page,
					],
					$row->files_name
				);

				$formatted = $this->linkRenderer->makeLink(
					SpecialPage::getTitleFor( 'CentralAuth', $row->files_user ),
					$row->files_user
				);
use MediaWiki\Linker\LinkRenderer;

	/** @var LinkRenderer */
	private $linkRenderer;
	function __construct( LinkRenderer $linkRenderer ) {
		$this->linkRenderer = $linkRenderer;
		$pager = new GlobalNewFilesPager( $this->getContext(), $this->linkRenderer );