From 407870bbddcc6b2cdf5edbc98c0d763c11b53092 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Taavi=20V=C3=A4=C3=A4n=C3=A4nen?= <hi@taavi.wtf>
Date: Thu, 21 Oct 2021 14:19:39 +0300
Subject: [PATCH] SECURITY: Send original client info on x-wiki requests

Bug: T285116
---
 includes/EchoHooks.php          |  6 +++---
 includes/ForeignWikiRequest.php | 31 ++++++++++++++++++++++++-------
 includes/NotifUser.php          | 10 ++++++----
 includes/api/ApiCrossWiki.php   |  2 +-
 4 files changed, 34 insertions(+), 15 deletions(-)

diff --git a/includes/EchoHooks.php b/includes/EchoHooks.php
index 974b381d..0aef852f 100644
--- a/includes/EchoHooks.php
+++ b/includes/EchoHooks.php
@@ -887,15 +887,15 @@ class EchoHooks implements RecentChange_saveHook {
 				$markAsReadIds = array_map( 'intval', $markAsReadIds );
 				// Look up the notifications on the foreign wiki
 				$notifUser = MWEchoNotifUser::newFromUser( $user );
-				$notifInfo = $notifUser->getForeignNotificationInfo( $markAsReadIds, $markAsReadWiki );
+				$notifInfo = $notifUser->getForeignNotificationInfo( $markAsReadIds, $markAsReadWiki, $request );
 				foreach ( $notifInfo as $id => $info ) {
 					$subtractions[$info['section']]++;
 				}
 
 				// Schedule a deferred update to mark these notifications as read on the foreign wiki
-				DeferredUpdates::addCallableUpdate( static function () use ( $user, $markAsReadIds, $markAsReadWiki ) {
+				DeferredUpdates::addCallableUpdate( static function () use ( $user, $markAsReadIds, $markAsReadWiki, $request ) {
 					$notifUser = MWEchoNotifUser::newFromUser( $user );
-					$notifUser->markReadForeign( $markAsReadIds, $markAsReadWiki );
+					$notifUser->markReadForeign( $markAsReadIds, $markAsReadWiki, $request );
 				} );
 			}
 		}
diff --git a/includes/ForeignWikiRequest.php b/includes/ForeignWikiRequest.php
index c943b000..f00233b0 100644
--- a/includes/ForeignWikiRequest.php
+++ b/includes/ForeignWikiRequest.php
@@ -49,14 +49,21 @@ class EchoForeignWikiRequest {
 
 	/**
 	 * Execute the request
+	 * @param WebRequest|null $originalRequest Original request data to be sent with these requests
 	 * @return array[] [ wiki => result ]
 	 */
-	public function execute() {
+	public function execute( ?WebRequest $originalRequest ) {
 		if ( !$this->canUseCentralAuth() ) {
 			return [];
 		}
 
-		$reqs = $this->getRequestParams( $this->method, [ $this, 'getQueryParams' ] );
+		$reqs = $this->getRequestParams(
+			$this->method,
+			function ( string $wiki ) use ( $originalRequest ) {
+				return $this->getQueryParams( $wiki, $originalRequest );
+			},
+			$originalRequest
+		);
 		return $this->doRequests( $reqs );
 	}
 
@@ -121,10 +128,11 @@ class EchoForeignWikiRequest {
 	 * This method fetches the tokens for all requested wikis at once and caches the result.
 	 *
 	 * @param string $wiki Name of the wiki to get a token for
+	 * @param WebRequest|null $originalRequest Original request data to be sent with these requests
 	 * @suppress PhanTypeInvalidCallableArraySize getRequestParams can take an array, too (phan bug)
 	 * @return string Token, or empty string if an unable to retrieve the token.
 	 */
-	protected function getCsrfToken( $wiki ) {
+	protected function getCsrfToken( $wiki, ?WebRequest $originalRequest ) {
 		if ( $this->csrfTokens === null ) {
 			$this->csrfTokens = [];
 			$reqs = $this->getRequestParams( 'GET', [
@@ -133,7 +141,7 @@ class EchoForeignWikiRequest {
 				'type' => $this->tokenType,
 				'format' => 'json',
 				'centralauthtoken' => $this->getCentralAuthToken( $this->user ),
-			] );
+			], $originalRequest );
 			$responses = $this->doRequests( $reqs );
 			foreach ( $responses as $w => $response ) {
 				if ( isset( $response['query']['tokens']['csrftoken'] ) ) {
@@ -156,9 +164,10 @@ class EchoForeignWikiRequest {
 	 * @param string $method 'GET' or 'POST'
 	 * @param array|callable $params Associative array of query string / POST parameters,
 	 *  or a callback that takes a wiki name and returns such an array
+	 * @param WebRequest|null $originalRequest Original request data to be sent with these requests
 	 * @return array[] Array of request parameters to pass to doRequests(), keyed by wiki name
 	 */
-	protected function getRequestParams( $method, $params ) {
+	protected function getRequestParams( $method, $params, ?WebRequest $originalRequest ) {
 		$apis = EchoForeignNotifications::getApiEndpoints( $this->wikis );
 		if ( !$apis ) {
 			return [];
@@ -172,6 +181,13 @@ class EchoForeignWikiRequest {
 				'url' => $api['url'],
 				$queryKey => is_callable( $params ) ? $params( $wiki ) : $params
 			];
+
+			if ( $originalRequest ) {
+				$reqs[$wiki]['headers'] = [
+					'X-Forwarded-For' => $originalRequest->getIP(),
+					'X-Original-User-Agent' => $originalRequest->getHeader( 'User-Agent' ),
+				];
+			}
 		}
 
 		return $reqs;
@@ -179,9 +195,10 @@ class EchoForeignWikiRequest {
 
 	/**
 	 * @param string $wiki Wiki name
+	 * @param WebRequest|null $originalRequest Original request data to be sent with these requests
 	 * @return array
 	 */
-	protected function getQueryParams( $wiki ) {
+	protected function getQueryParams( $wiki, ?WebRequest $originalRequest ) {
 		$extraParams = [];
 		if ( $this->wikiParam ) {
 			// Only request data from that specific wiki, or they'd all spawn
@@ -189,7 +206,7 @@ class EchoForeignWikiRequest {
 			$extraParams[$this->wikiParam] = $wiki;
 		}
 		if ( $this->method === 'POST' ) {
-			$extraParams['token'] = $this->getCsrfToken( $wiki );
+			$extraParams['token'] = $this->getCsrfToken( $wiki, $originalRequest );
 		}
 
 		return [
diff --git a/includes/NotifUser.php b/includes/NotifUser.php
index 3611b439..b89f718a 100644
--- a/includes/NotifUser.php
+++ b/includes/NotifUser.php
@@ -376,8 +376,9 @@ class MWEchoNotifUser {
 	 *
 	 * @param int[] $eventIds Event IDs to mark as read
 	 * @param string $wiki Wiki name
+	 * @param WebRequest|null $originalRequest Original request data to be sent with these requests
 	 */
-	public function markReadForeign( array $eventIds, $wiki ) {
+	public function markReadForeign( array $eventIds, $wiki, ?WebRequest $originalRequest = null ) {
 		$foreignReq = new EchoForeignWikiRequest(
 			$this->mUser,
 			[
@@ -388,7 +389,7 @@ class MWEchoNotifUser {
 			'wikis',
 			'csrf'
 		);
-		$foreignReq->execute();
+		$foreignReq->execute( $originalRequest );
 	}
 
 	/**
@@ -396,9 +397,10 @@ class MWEchoNotifUser {
 	 *
 	 * @param int[] $eventIds Event IDs to look up. Only unread notifications can be found.
 	 * @param string $wiki Wiki name
+	 * @param WebRequest|null $originalRequest Original request data to be sent with these requests
 	 * @return array[] Array of notification data as returned by api.php, keyed by event ID
 	 */
-	public function getForeignNotificationInfo( array $eventIds, $wiki ) {
+	public function getForeignNotificationInfo( array $eventIds, $wiki, ?WebRequest $originalRequest = null ) {
 		$foreignReq = new EchoForeignWikiRequest(
 			$this->mUser,
 			[
@@ -411,7 +413,7 @@ class MWEchoNotifUser {
 			[ $wiki ],
 			'notwikis'
 		);
-		$foreignResults = $foreignReq->execute();
+		$foreignResults = $foreignReq->execute( $originalRequest );
 		$list = $foreignResults[$wiki]['query']['notifications']['list'] ?? [];
 
 		$result = [];
diff --git a/includes/api/ApiCrossWiki.php b/includes/api/ApiCrossWiki.php
index b3f75b7e..b89c3bf2 100644
--- a/includes/api/ApiCrossWiki.php
+++ b/includes/api/ApiCrossWiki.php
@@ -36,7 +36,7 @@ trait ApiCrossWiki {
 			$this->getModulePrefix() . 'wikis',
 			$tokenType !== false ? $tokenType : null
 		);
-		return $foreignReq->execute();
+		return $foreignReq->execute( $this->getRequest() );
 	}
 
 	/**
-- 
2.33.0

