5.4.1でパーマリンクの仕様が一部変更された件

説明

先月末にリリースされたバージョン5.4.1のアップデートにより、一部のサイトで投稿ページが従来通りに表示されなくなる不具合が発生し、サポートサイトにレポートが上がりました。この件について、ちょっとモヤモヤしたので自分なりに考えてみました。

未認証でプライベート投稿が閲覧できる

今回発生した不具合は「WordPress 5.4.1」のセキュリティ更新にある「特定のプライベートな投稿が認証されていない状態で閲覧できてしまう問題」の対処が影響している。具体的には、投稿のパーマリンクに(投稿日時)の年月日時分秒パラメータをすべて設定していた場合、5.4.0までは投稿ページのテンプレート(single.phpなど)が適用され、そこで同じ投稿日時の投稿をすべて表示するようになっており、その中にはプライベートな投稿も含まれる可能性があった。

このセキュリティ問題が発生する主な条件は2つだと思う。

  1. 公開している投稿情報とプライベートな投稿情報の投稿日時が同一で、公開している投稿情報の投稿IDがプライベートな投稿情報の投稿IDよりも小さい(IDの関係が逆の場合は404になる)
  2. 投稿ページのテンプレートで投稿情報をループして出力(表示)

投稿編集ページでは、投稿日時の「年月日時分」を設定可能だが、「秒」はできない。このため、データベースを直接操作するかプラグラムで意識的に設定しない限り同一の投稿日時を設定するのは難しい。

投稿ページのテンプレートのループ処理に関しては、標準テーマのTwentyシリーズが伝統的に採用しており、それらのテーマを参考に作られているテーマも多数あると思われるため多くのサイトがこの問題を抱えていた。

なお、ユーザーのリクエストURLについては、パーマリンクに設定されたものだけでなく、クエリーパラメータに年月日時分秒のすべてが指定された場合も同様の問題が発生する。

具体的な修正内容とその影響

この問題を修正するためのパッチは、tracの「Changeset 47641」である。修正内容はシンプルで、これまではクエリーパラメータに年月日時分秒のすべてが指定された場合はis_singleフラグがtrueになっていたが、この修正によりis_singleフラグ(プロパティ)は初期値のままfalseとなる。バージョンアップ前後で影響を受けたテンプレート判定用フラグは次の通りである。

フラグ~5.4.05.4.1
is_singletruefalse
is_singulartruefalse
is_archivefalsetrue
is_datefalsetrue
is_timefalsetrue

ほかの修正プランはなかったのか?

上記の対応により、投稿情報を出力するためのテンプレートは、投稿ページ用ではなく、アーカイブページ用が適用されるようになり、そこにはプライベートな投稿情報は出力されない。これで今回の問題は解決といえるのかもしれないが、そもそもis_singleフラグがtrueの場合になぜプライベートな投稿情報を表示される状況になっていたのか。その理由が気になったので、class-wp-query.phpのソースコードに目を通すことにした。

原因と思われる個所は、get_postsメソッドにあった。

// Check post status to determine if post should be displayed.
if ( ! empty( $this->posts ) && ( $this->is_single || $this->is_page ) ) {
	$status = get_post_status( $this->posts[0] );

	if ( 'attachment' === $this->posts[0]->post_type && 0 === (int) $this->posts[0]->post_parent ) {
		$this->is_page       = false;
		$this->is_single     = true;
		$this->is_attachment = true;
	}

	// If the post_status was specifically requested, let it pass through.
	if ( ! in_array( $status, $q_status ) ) {
		$post_status_obj = get_post_status_object( $status );

		if ( $post_status_obj && ! $post_status_obj->public ) {
			if ( ! is_user_logged_in() ) {
				// User must be logged in to view unpublished posts.
				$this->posts = array();
			} else {
				if ( $post_status_obj->protected ) {
					// User must have edit permissions on the draft to preview.
					if ( ! current_user_can( $edit_cap, $this->posts[0]->ID ) ) {
						$this->posts = array();
					} else {
						$this->is_preview = true;
						if ( 'future' != $status ) {
							$this->posts[0]->post_date = current_time( 'mysql' );
						}
					}
				} elseif ( $post_status_obj->private ) {
					if ( ! current_user_can( $read_cap, $this->posts[0]->ID ) ) {
						$this->posts = array();
					}
				} else {
					$this->posts = array();
				}
			}
		} elseif ( ! $post_status_obj ) {
			// Post status is not registered, assume it's not public.
			if ( ! current_user_can( $edit_cap, $this->posts[0]->ID ) ) {
				$this->posts = array();
			}
		}
	}

	if ( $this->is_preview && $this->posts && current_user_can( $edit_cap, $this->posts[0]->ID ) ) {
		/**
		 * Filters the single post for preview mode.
		 *
		 * @since 2.7.0
		 *
		 * @param WP_Post  $post_preview  The Post object.
		 * @param WP_Query $this          The WP_Query instance (passed by reference).
		 */
		$this->posts[0] = get_post( apply_filters_ref_array( 'the_preview', array( $this->posts[0], &$this ) ) );
	}
}

ここでは検索できた1件目の投稿情報のみを評価しており、2件目以降はスルーされている。これにより、プライベートな投稿情報が1件目の場合は404ページに、2件目以降の場合はそれらの投稿情報は維持され、結果的に未認証でも閲覧できることになった。今回の問題の原因はここにある。

先の修正内容は、ここでの処理を回避するためにis_singleフラグをtrueにしないということのようだ。ただし、1件目と同様の処理を2件目以降も行っても問題を対処できる。このような代案があることを踏まえ、今回の修正に至った経緯が少し気になった。


最終更新 : 2020年05月12日 15:07


お勧め

delete_option(2019年4月24日 更新)

bool delete_option( string $option )
サイトオプションを削除する。

flush_rewrite_rules(2015年9月24日 更新)

void flush_rewrite_rules( [ bool $hard = true ] )
リライトルールを更新する。

wp_nonce_tick(2014年5月20日 更新)

int wp_nonce_tick()
nonce用の時間依存値を取得する。

get_privacy_policy_url(2018年5月27日 更新)

string get_privacy_policy_url()
プライバシーポリシーページのURLを取得する。

delete_metadata(2016年2月23日 更新)

bool delete_metadata( string $meta_type, int $object_id, string $meta_key [ , mixed $meta_value = '' [ , bool $delete_all = false ] ] )
メタ情報の値を削除する。