CVE-2021-41800: Allocation of Resources Without Limits or Throttling (CWE-770)
value: MediaWiki before 1.36.2 allows a denial of service (resource consumption because of lengthy query processing time). Visiting Special:Contributions can sometimes result in a long running SQL query because PoolCounter protection is mishandled.
CWE description: The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
CVSS v3 base score: 5.3
Published: 2021-10-11T08:15Z
Repository: https://github.com/wikimedia/mediawiki
Language: PHP
Files changed: includes/specials/SpecialContributions.php
Commit date: 2021-09-30 22:19:16 +0000
Commit message: SECURITY: fix PoolCounter protection of Special:Contributions

The call to $pager->getNumRows() itself triggers execution of
the database query backing the page, so, that call must be inside
the callback given to PoolCounterWorkViaCallback.

CVE-2021-41800

Bug: T284419
Change-Id: I8b7b41a355be265389a4a8c9ea91301d4e23ae1b

Diff: diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php
index 44bfe4a08db..963dc4623b1 100644
--- a/includes/specials/SpecialContributions.php
+++ b/includes/specials/SpecialContributions.php
@@ -308,36 +308,38 @@ class SpecialContributions extends IncludableSpecialPage {
 			if ( IPUtils::isValidRange( $target ) && !$pager->isQueryableRange( $target ) ) {
 				// Valid range, but outside CIDR limit.
 				$limits = $this->getConfig()->get( 'RangeContributionsCIDRLimit' );
 				$limit = $limits[ IPUtils::isIPv4( $target ) ? 'IPv4' : 'IPv6' ];
 				$out->addWikiMsg( 'sp-contributions-outofrange', $limit );
-			} elseif ( !$pager->getNumRows() ) {
-				$out->addWikiMsg( 'nocontribs', $target );
 			} else {
 				// @todo We just want a wiki ID here, not a "DB domain", but
 				// current status of MediaWiki conflates the two. See T235955.
 				$poolKey = $this->loadBalancer->getLocalDomainID() . ':SpecialContributions:';
 				if ( $this->getUser()->isAnon() ) {
 					$poolKey .= 'a:' . $this->getUser()->getName();
 				} else {
 					$poolKey .= 'u:' . $this->getUser()->getId();
 				}
 				$work = new PoolCounterWorkViaCallback( 'SpecialContributions', $poolKey, [
-					'doWork' => function () use ( $pager, $out ) {
-						# Show a message about replica DB lag, if applicable
-						$lag = $pager->getDatabase()->getSessionLagStatus()['lag'];
-						if ( $lag > 0 ) {
-							$out->showLagWarning( $lag );
+					'doWork' => function () use ( $pager, $out, $target ) {
+						if ( !$pager->getNumRows() ) {
+							$out->addWikiMsg( 'nocontribs', $target );
+						} else {
+							# Show a message about replica DB lag, if applicable
+							$lag = $pager->getDatabase()->getSessionLagStatus()['lag'];
+							if ( $lag > 0 ) {
+								$out->showLagWarning( $lag );
+							}
+
+							$output = $pager->getBody();
+							if ( !$this->including() ) {
+								$output = $pager->getNavigationBar() .
+									$output .
+									$pager->getNavigationBar();
+							}
+							$out->addHTML( $output );
 						}
-
-						$output = $pager->getBody();
-						if ( !$this->including() ) {
-							$output = $pager->getNavigationBar() .
-								$output .
-								$pager->getNavigationBar();
-						}
-						$out->addHTML( $output );
 					},
 					'error' => function () use ( $out ) {
 						$msg = $this->getUser()->isAnon()
 							? 'sp-contributions-concurrency-ip'
 							: 'sp-contributions-concurrency-user';

Vulnerable code: 			} elseif ( !$pager->getNumRows() ) {
				$out->addWikiMsg( 'nocontribs', $target );
					'doWork' => function () use ( $pager, $out ) {
						# Show a message about replica DB lag, if applicable
						$lag = $pager->getDatabase()->getSessionLagStatus()['lag'];
						if ( $lag > 0 ) {
							$out->showLagWarning( $lag );

						$output = $pager->getBody();
						if ( !$this->including() ) {
							$output = $pager->getNavigationBar() .
								$output .
								$pager->getNavigationBar();
						}
						$out->addHTML( $output );
Fixed code: 					'doWork' => function () use ( $pager, $out, $target ) {
						if ( !$pager->getNumRows() ) {
							$out->addWikiMsg( 'nocontribs', $target );
						} else {
							# Show a message about replica DB lag, if applicable
							$lag = $pager->getDatabase()->getSessionLagStatus()['lag'];
							if ( $lag > 0 ) {
								$out->showLagWarning( $lag );
							}

							$output = $pager->getBody();
							if ( !$this->including() ) {
								$output = $pager->getNavigationBar() .
									$output .
									$pager->getNavigationBar();
							}
							$out->addHTML( $output );