From d8752f4d813a3a031247d01cdfb9c20f978e2609 Mon Sep 17 00:00:00 2001
From: a smart kitten <sierra-quirk-suave@duck.com>
Date: Thu, 24 Apr 2025 16:39:02 -0500
Subject: [PATCH] SECURITY: Don't add the 'Recreated' tag if the target of a
 title's deletion log entry is hidden

Why:

- MediaWiki currently tags edits that create a new page at a
  previously-deleted title with 'Recreated', whether or not the target
  of the deletion log entry for that title has been hidden.
- However, if the target of the log events relating to a title have been
  hidden, it is no longer publicly known that a page with that title
  used to exist.
- Because of this, in a situation where a page is created at a title
  that has previously existed but has had the targets of its log events
  hidden, MediaWiki - in tagging the edit creating the new page with
  'Recreated' - currently leaks the information that a page at that
  title has previously existed.

What:

- Added a condition to the database query in maybeAddRecreateChangeTag()
  so that it only matches deletion log entries that don't have a hidden
  target (LogPage::DELETED_ACTION).
- Added a test case to check that edits are no longer tagged as
  'Recreated' when the target of a title's deletion log entry has
  been hidden.
- Modified existing tests to remove calls to
  ManualLogEntry::setDeleted(), and edited test titles for clarity.

Bug: T385792
Change-Id: Ifb8299581e1162de35d5741b193610b09253bf9b
---
 includes/Storage/DerivedPageDataUpdater.php   | 11 ++++--
 .../Storage/DerivedPageDataUpdaterTest.php    | 35 ++++++++++++++++---
 2 files changed, 40 insertions(+), 6 deletions(-)

diff --git a/includes/Storage/DerivedPageDataUpdater.php b/includes/Storage/DerivedPageDataUpdater.php
index 83d68b30966..8e0f9792f37 100644
--- a/includes/Storage/DerivedPageDataUpdater.php
+++ b/includes/Storage/DerivedPageDataUpdater.php
@@ -23,6 +23,7 @@ namespace MediaWiki\Storage;
 use InvalidArgumentException;
 use LogicException;
 use MediaWiki\ChangeTags\ChangeTags;
+use LogPage;
 use MediaWiki\ChangeTags\ChangeTagsStore;
 use MediaWiki\Config\ServiceOptions;
 use MediaWiki\Content\Content;
@@ -1770,14 +1771,20 @@ class DerivedPageDataUpdater implements LoggerAwareInterface, PreparedUpdate {
 	 * @param int $revisionId
 	 */
 	private function maybeAddRecreateChangeTag( WikiPage $wikiPage, int $revisionId ) {
-		if ( $this->loadbalancerFactory->getReplicaDatabase()->newSelectQueryBuilder()
+		$replicaDB = $this->loadbalancerFactory->getReplicaDatabase();
+
+		if ( $replicaDB->newSelectQueryBuilder()
 				->select( [ '1' ] )
 				->from( 'logging' )
 				->where( [
 					'log_type' => 'delete',
 					'log_title' => $wikiPage->getTitle()->getDBkey(),
 					'log_namespace' => $wikiPage->getNamespace(),
-				] )->caller( __METHOD__ )->limit( 1 )->fetchField() ) {
+				] )
+				->where(
+					$replicaDB->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) .
+						' != ' . LogPage::DELETED_ACTION // T385792
+				)->caller( __METHOD__ )->limit( 1 )->fetchField() ) {
 			$this->changeTagsStore->addTags(
 				[ ChangeTags::TAG_RECREATE ],
 				null,
diff --git a/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php b/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php
index 91614f56649..e3ac5a194e2 100644
--- a/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php
+++ b/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php
@@ -1390,7 +1390,7 @@ class DerivedPageDataUpdaterTest extends MediaWikiIntegrationTestCase {
 	 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates()
 	 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::maybeAddRecreateChangeTag
 	 */
-	public function testDoUpdatesTagsEditAsRecreatedWhenDeletedLogEntry() {
+	public function testDoUpdatesTagsEditAsRecreatedWhenDeletionLogEntry() {
 		$page = $this->getPage( __METHOD__ );
 		$title = $this->getTitle( __METHOD__ );
 
@@ -1400,7 +1400,6 @@ class DerivedPageDataUpdaterTest extends MediaWikiIntegrationTestCase {
 		$deleteLogEntry = new ManualLogEntry( 'delete', 'delete' );
 		$deleteLogEntry->setPerformer( $this->getTestUser()->getUser() );
 		$deleteLogEntry->setTarget( $title );
-		$deleteLogEntry->setDeleted( LogPage::DELETED_ACTION );
 		$logId = $deleteLogEntry->insert( $this->getDb() );
 		$deleteLogEntry->publish( $logId );
 
@@ -1426,7 +1425,6 @@ class DerivedPageDataUpdaterTest extends MediaWikiIntegrationTestCase {
 		$deleteLogEntry = new ManualLogEntry( 'delete', 'delete' );
 		$deleteLogEntry->setPerformer( $this->getTestUser()->getUser() );
 		$deleteLogEntry->setTarget( $title );
-		$deleteLogEntry->setDeleted( LogPage::DELETED_ACTION );
 		$logId = $deleteLogEntry->insert( $this->getDb() );
 		$deleteLogEntry->publish( $logId );
 
@@ -1475,10 +1473,39 @@ class DerivedPageDataUpdaterTest extends MediaWikiIntegrationTestCase {
 	 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates()
 	 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::maybeAddRecreateChangeTag
 	 */
-	public function testDoUpdatesDoesNotTagEditAsRecreatedWhenNoDeletedLogEntry() {
+	public function testDoUpdatesDoesNotTagEditAsRecreatedWhenNoDeletionLogEntry() {
+		$page = $this->getPage( __METHOD__ );
+
+		$content = [ SlotRecord::MAIN => new WikitextContent( 'rev ID ver #1: {{REVISIONID}}' ) ];
+		$rev = $this->createRevision( $page, 'first', $content );
+
+		$this->assertSame( [], $this->getServiceContainer()->getChangeTagsStore()->getTags(
+			$this->getDb(), null, $rev->getId() ) );
+	}
+
+	/**
+	 * See T385792
+	 *
+	 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates()
+	 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::maybeAddRecreateChangeTag
+	 */
+	public function testDoUpdatesDoesNotTagEditAsRecreatedWhenDeletionLogEntryActionHidden() {
 		$page = $this->getPage( __METHOD__ );
+		$title = $this->getTitle( __METHOD__ );
 
 		$content = [ SlotRecord::MAIN => new WikitextContent( 'rev ID ver #1: {{REVISIONID}}' ) ];
+
+		// create a deletion log entry
+		$deleteLogEntry = new ManualLogEntry( 'delete', 'delete' );
+		$deleteLogEntry->setPerformer( $this->getTestUser()->getUser() );
+		$deleteLogEntry->setTarget( $title );
+
+		// hide the target of the deletion log entry
+		$deleteLogEntry->setDeleted( LogPage::DELETED_ACTION );
+
+		$logId = $deleteLogEntry->insert( $this->getDb() );
+		$deleteLogEntry->publish( $logId );
+
 		$rev = $this->createRevision( $page, 'first', $content );
 
 		$this->assertSame( [], $this->getServiceContainer()->getChangeTagsStore()->getTags(
-- 
2.49.0

