From a3572b31175b0f5a7c562680e393e5130f6516b6 Mon Sep 17 00:00:00 2001
From: DannyS712 <dannys712.enwiki@gmail.com>
Date: Thu, 10 Dec 2020 18:13:56 -0500
Subject: [PATCH] SECURITY: Act like users don't exist if hidden from viewer

When viewing Special:Contributions for a hidden user and
a missing user, or the user page of a hidden user and a
missing user, if the viewer cannot see hidden users
the output should be the same for hidden users and
missing users.

To that end
* In OutputPage.php, only set the `wgRelevantUserName` javascript
  variable if the user is not hidden, or the viewer can see hidden
  users
* In Article.php, show the `userpage-userdoesnotexist-view` on user
  pages of hidden users if the viewer cannot see hidden users
* In Skin.php, do not add user-specific sidebar links (contributions,
  logs, mute, etc.) if the user is hidden and the viewer cannot see
  hidden users
* In SpecialContributions.php, stop calling Skin::setRelevantUser
  for non-existing users, so that callers of Skin::getRelevantUser
  can ignore users that are hidden from the viewer without creating
  divergent behavior
* In SpecialContributions.php, for users that do exist but are
  hidden from the viewer, don't show `sp-contributions-footer`,
  but do show `contributions-userdoesnotexist`

Bug: T120883
Change-Id: I83b723402f315447bc4b50992e28620e3daace8f
---
 includes/OutputPage.php                    |  6 ++++-
 includes/page/Article.php                  |  8 +++++++
 includes/skins/Skin.php                    | 12 ++++++++++
 includes/specials/SpecialContributions.php | 28 ++++++++++++++++++++--
 4 files changed, 51 insertions(+), 3 deletions(-)

diff --git a/includes/OutputPage.php b/includes/OutputPage.php
index 03a2fd2fc6..5f12b0cad7 100644
--- a/includes/OutputPage.php
+++ b/includes/OutputPage.php
@@ -3373,7 +3373,11 @@ class OutputPage extends ContextSource {
 		if ( $title->isMainPage() ) {
 			$vars['wgIsMainPage'] = true;
 		}
-		if ( $relevantUser ) {
+		if ( $relevantUser && ( !$relevantUser->isHidden() ||
+			$services->getPermissionManager()->userHasRight( $user, 'hideuser' ) )
+		) {
+			// T120883 if the user is hidden and the viewer cannot see
+			// hidden users, pretend like it does not exist at all.
 			$vars['wgRelevantUserName'] = $relevantUser->getName();
 		}
 		// End of stable config vars
diff --git a/includes/page/Article.php b/includes/page/Article.php
index 149c3e2ed0..b3a6ea0749 100644
--- a/includes/page/Article.php
+++ b/includes/page/Article.php
@@ -1372,6 +1372,14 @@ class Article implements Page {
 			$ip = User::isIP( $rootPart );
 			$block = DatabaseBlock::newFromTarget( $user, $user );
 
+			if ( $user && $user->isLoggedIn() && $user->isHidden() &&
+				!$services->getPermissionManager()
+					->userHasRight( $this->getContext()->getUser(), 'hideuser' )
+			) {
+				// T120883 if the user is hidden and the viewer cannot see hidden
+				// users, pretend like it does not exist at all.
+				$user = false;
+			}
 			if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
 				$outputPage->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
 					[ 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) ] );
diff --git a/includes/skins/Skin.php b/includes/skins/Skin.php
index d7a6f3baa6..454025c59c 100644
--- a/includes/skins/Skin.php
+++ b/includes/skins/Skin.php
@@ -1609,6 +1609,18 @@ abstract class Skin extends ContextSource {
 		}
 
 		$user = $this->getRelevantUser();
+		// The relevant user should only be set if it exists. However, if it exists but is hidden,
+		// and the viewer cannot see hidden users, this exposes the fact that the user exists;
+		// pretend like the user does not exist in such cases, by setting $user to null, which
+		// is what getRelevantUser returns if there is no user set (though it is documented as
+		// always returning a User...) See T120883
+		if ( $user && $user->isRegistered() && $user->isHidden() &&
+			 !MediaWikiServices::getInstance()
+				->getPermissionManager()->userHasRight( $this->getUser(), 'hideuser' )
+		) {
+			$user = null;
+		}
+
 		if ( $user ) {
 			$rootUser = $user->getName();
 
diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php
index 960f2b1abc..c91795fa4f 100644
--- a/includes/specials/SpecialContributions.php
+++ b/includes/specials/SpecialContributions.php
@@ -180,7 +180,15 @@ class SpecialContributions extends IncludableSpecialPage {
 
 			# For IP ranges, we want the contributionsSub, but not the skin-dependent
 			# links under 'Tools', which may include irrelevant links like 'Logs'.
-			if ( !IPUtils::isValidRange( $target ) ) {
+			if ( !IPUtils::isValidRange( $target ) &&
+				( User::isIP( $target ) || $userObj->isRegistered() )
+			) {
+				// Don't add non-existent users, because hidden users
+				// that we add here will be removed later to pretend
+				// that they don't exist, and if users that actually don't
+				// exist are added here and then not removed, it exposes
+				// which users exist and are hidden vs. which actually don't
+				// exist. But, do set the relevant user for single IPs.
 				$this->getSkin()->setRelevantUser( $userObj );
 			}
 		}
@@ -360,7 +368,14 @@ class SpecialContributions extends IncludableSpecialPage {
 			} elseif ( $userObj->isAnon() ) {
 				// No message for non-existing users
 				$message = '';
+			} elseif( $userObj->isHidden() &&
+				!$this->permissionManager->userHasRight( $this->getUser(), 'hideuser' )
+			) {
+				// User is registered, but make sure that the viewer can see them, to avoid
+				// having different behavior for missing and hidden users; see T120883
+				$message = '';
 			} else {
+				// Not hidden, or hidden but the viewer can still see it
 				$message = 'sp-contributions-footer';
 			}
 
@@ -380,7 +395,16 @@ class SpecialContributions extends IncludableSpecialPage {
 	 * Could be combined.
 	 */
 	protected function contributionsSub( $userObj ) {
-		if ( $userObj->isAnon() ) {
+		$isAnon = $userObj->isAnon();
+		if ( !$isAnon && $userObj->isHidden() &&
+			!$this->permissionManager->userHasRight( $this->getUser(), 'hideuser' )
+		) {
+			// T120883 if the user is hidden and the viewer cannot see hidden
+			// users, pretend like it does not exist at all.
+			$isAnon = true;
+		}
+
+		if ( $isAnon ) {
 			// Show a warning message that the user being searched for doesn't exist.
 			// User::isIP returns true for IP address and usemod IPs like '123.123.123.xxx',
 			// but returns false for IP ranges. We don't want to suggest either of these are
-- 
2.25.1

