CVE-2026-9008

Page-list <= 6.2 - Missing Authorization to Authenticated (Contributor+) Sensitive Information Disclosure via Shortcode Attributes

mediumMissing Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
6.3
Patched in
1d
Time to patch

Description

The Page-list plugin for WordPress is vulnerable to Missing Authorization in all versions up to, and including, 6.2. This is due to the pagelist_unqprfx_ext_shortcode() function (the [pagelist_ext] / [pagelistext] shortcode) accepting attacker-controlled post_status, post_type, and show_meta_key attributes and passing them directly into get_pages() and get_post_meta() with no capability check verifying that the rendering user is permitted to read the matched objects. When the current post has no child pages, the shortcode re-issues the query with child_of => 0, broadening it to every page on the site matching the supplied status/type. This makes it possible for authenticated attackers, with contributor-level access and above, to disclose the titles, body content/excerpts, and arbitrary post meta of unrelated private and draft pages by inserting the shortcode into a contributor-authored draft and previewing it.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Low
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=6.2
PublishedJune 5, 2026
Last updatedJune 6, 2026
Affected pluginpage-list

What Changed in the Fix

Changes introduced in v6.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-9008 (Page-list Plugin) ## 1. Vulnerability Summary The **Page-list** plugin for WordPress (versions <= 6.2) contains a **Missing Authorization** vulnerability. The `pagelist_unqprfx_ext_shortcode()` function, which handles the `[pagelist_ext]` shortcode, allo…

Show full research plan

Exploitation Research Plan: CVE-2026-9008 (Page-list Plugin)

1. Vulnerability Summary

The Page-list plugin for WordPress (versions <= 6.2) contains a Missing Authorization vulnerability. The pagelist_unqprfx_ext_shortcode() function, which handles the [pagelist_ext] shortcode, allows users to specify post_status, post_type, and show_meta_key attributes without verifying if the user viewing the rendered output has the authority to access that information.

By inserting a crafted shortcode into a post (even a draft), an authenticated attacker with Contributor-level access can disclose sensitive information—including titles, full content, and custom field (meta) values—from private or draft posts created by other users. This is exacerbated by a "fallback" mechanism: if the current post has no child pages, the shortcode automatically performs a site-wide query, exposing unrelated content.

2. Attack Vector Analysis

  • Vulnerable Shortcode: [pagelist_ext] (and alias [pagelistext]).
  • Vulnerable Function: pagelist_unqprfx_ext_shortcode() in inc/shortcode-pagelist-ext.php.
  • Vulnerable Attributes: post_status, post_type, show_meta_key.
  • Authentication Level: Authenticated (Contributor+). Contributors can create posts and view previews, which is sufficient to trigger the shortcode rendering.
  • Mechanism: The shortcode parameters are passed to get_pages() and get_post_meta() without capability checks (e.g., current_user_can('read_private_posts')).

3. Code Flow

  1. Entry Point: A user with Contributor role creates or edits a post/page and inserts the [pagelist_ext ...] shortcode.
  2. Rendering: When the post is previewed or viewed, WordPress calls do_shortcode(), which invokes pagelist_unqprfx_ext_shortcode( $atts ).
  3. Parameter Extraction: The function uses extract( shortcode_atts( ..., $atts ) ) to populate variables like $post_status and $show_meta_key directly from user input.
  4. Query Logic:
    • The function builds $page_list_ext_args.
    • If child_of is not provided, it defaults to the current post ID ($post->ID).
    • It calls get_pages( $page_list_ext_args ).
  5. Vulnerable Fallback: If count( $list_pages ) == 0 (standard for a new contributor draft), it re-runs the query using $page_list_ext_args_all, which sets 'child_of' => 0, targeting the entire site.
  6. Information Leak (Sink):
    • The function iterates through the retrieved pages.
    • It retrieves content via $page->post_content.
    • It retrieves meta data via `get_post_meta( $page->ID, $show_meta_key
Research Findings
Static analysis — not yet PoC-verified

Summary

The Page-list plugin for WordPress is vulnerable to sensitive information disclosure because the [pagelist_ext] shortcode accepts user-defined attributes like post_status and show_meta_key without verifying the viewer's permissions. This allows authenticated attackers with Contributor-level access to disclose the titles, full body content, and private metadata of draft or private posts by previewing a post containing the crafted shortcode.

Vulnerable Code

// inc/shortcode-pagelist-ext.php

	function pagelist_unqprfx_ext_shortcode( $atts ) {
		global $post, $pagelist_unq_settings;
		$return = '';
		extract( shortcode_atts( array(
			// ... (truncated attributes)
			'post_type' => 'page',
			'post_status' => 'publish',
			'show_meta_key' => '',
			'meta_template' => '%meta%'
		), $atts ) );

---

// inc/shortcode-pagelist-ext.php approx line 141

		$list_pages = get_pages( $page_list_ext_args );
		if ( count( $list_pages ) == 0 && $post_status === 'publish' ) { // if there is no subpages
			// Only fall back to a site-wide query for public content to avoid
			// disclosing unrelated private/draft pages (Wordfence ticket 454582).
			$list_pages = get_pages( $page_list_ext_args_all ); // we are showing all pages
		}

Security Fix

Only in /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.2: .gitattributes
Only in /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.2: .gitignore
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.2/inc/shortcode-pagelist-ext.php /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.3/inc/shortcode-pagelist-ext.php
--- /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.2/inc/shortcode-pagelist-ext.php	2026-05-26 15:10:44.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.3/inc/shortcode-pagelist-ext.php	2026-05-29 03:09:30.000000000 +0000
@@ -29,7 +29,6 @@
 			'number' => '',
 			'offset' => 0,
 			'post_type' => 'page',
-			'post_status' => 'publish',
 			'class' => '',
 			'strip_tags' => 1,
 			'strip_shortcodes' => 1,
@@ -55,30 +54,26 @@
 			$child_of = isset($post->ID) ? $post->ID : 0;
 		}
 
-		// --- Security hardening (CVE: Wordfence ticket 454582) ---
+		// --- Security hardening (Wordfence ticket 454582) ---
+		// post_status is no longer accepted from the shortcode: only published
+		// content is ever listed. This removes the unauthorized-disclosure
+		// vector for private/draft pages entirely.
+		$post_status = 'publish';
 		// Restrict post_type to public post types; fall back to 'page' otherwise.
 		$pagelist_ext_allowed_types = get_post_types( array( 'public' => true ) );
 		if ( ! is_array( $pagelist_ext_allowed_types ) || ! in_array( $post_type, $pagelist_ext_allowed_types, true ) ) {
 			$post_type = 'page';
 		}
-		// Restrict post_status: only allow non-public statuses if the current
-		// user has the capability to read those posts for the requested type.
-		$pagelist_ext_requested_status = $post_status;
-		$post_status = 'publish';
-		$pagelist_ext_pt_obj = get_post_type_object( $post_type );
-		if ( $pagelist_ext_requested_status === 'private'
-			&& $pagelist_ext_pt_obj
-			&& current_user_can( $pagelist_ext_pt_obj->cap->read_private_posts ) ) {
-			$post_status = 'private';
-		} elseif ( in_array( $pagelist_ext_requested_status, array( 'draft', 'pending', 'future', 'trash' ), true )
-			&& $pagelist_ext_pt_obj
-			&& current_user_can( $pagelist_ext_pt_obj->cap->edit_others_posts ) ) {
-			$post_status = $pagelist_ext_requested_status;
-		}
-		// Disallow protected (underscore-prefixed) meta keys unless the user
-		// can edit others' posts. Prevents disclosure of arbitrary post meta.
-		if ( $show_meta_key !== '' && is_protected_meta( $show_meta_key, 'post' ) && ! current_user_can( 'edit_others_posts' ) ) {
-			$show_meta_key = '';
+		// Restrict show_meta_key to prevent disclosure of arbitrary or protected
+		// post meta. Users who cannot edit posts may only read meta keys that are
+		// registered as publicly visible (show_in_rest) and are not protected.
+		if ( $show_meta_key !== '' && ! current_user_can( 'edit_posts' ) ) {
+			$pagelist_ext_registered_meta = get_registered_meta_keys( 'post' );
+			if ( is_protected_meta( $show_meta_key, 'post' )
+				|| ! isset( $pagelist_ext_registered_meta[ $show_meta_key ] )
+				|| empty( $pagelist_ext_registered_meta[ $show_meta_key ]['show_in_rest'] ) ) {
+				$show_meta_key = '';
+			}
 		}
 
 		$page_list_ext_args = array(
@@ -156,14 +151,6 @@
 		$offset_count = 0;
 		if ( $list_pages !== false && count( $list_pages ) > 0 ) {
 			foreach($list_pages as $page){
-				// Skip pages the current user is not permitted to read
-				// (Wordfence ticket 454582). Only enforced for non-public
-				// statuses; published pages are visible to everyone and
-				// current_user_can( 'read_post', ... ) returns false for
-				// anonymous viewers, which would hide published content.
-				if ( $post_status !== 'publish' && ! current_user_can( 'read_post', $page->ID ) ) {
-					continue;
-				}
 				$count++;
 				$offset_count++;
 				if ( !empty( $offset ) && is_numeric( $offset ) && $offset_count <= $offset ) {
Only in /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.3/inc: shortcode-pagelist-ext.php.orig
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.2/inc/shortcode-pagelist.php /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.3/inc/shortcode-pagelist.php
--- /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.2/inc/shortcode-pagelist.php	2026-05-26 15:10:44.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.3/inc/shortcode-pagelist.php	2026-05-29 03:09:30.000000000 +0000
@@ -27,8 +27,7 @@
 			'sort_order'   => $sort_order,
 			'link_before'  => esc_html($link_before),
 			'link_after'   => esc_html($link_after),
-			'post_type'    => $post_type,
-			'post_status'  => $post_status
+			'post_type'    => $post_type
 		);
 		$list_pages = wp_list_pages( $page_list_args );
 
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.2/inc/shortcode-siblings.php /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.3/inc/shortcode-siblings.php
--- /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.2/inc/shortcode-siblings.php	2026-05-26 15:10:44.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.3/inc/shortcode-siblings.php	2026-05-29 03:09:30.000000000 +0000
@@ -31,8 +31,7 @@
 			'sort_order'   => $sort_order,
 			'link_before'  => esc_html($link_before),
 			'link_after'   => esc_html($link_after),
-			'post_type'    => $post_type,
-			'post_status'  => $post_status
+			'post_type'    => $post_type
 		);
 		$list_pages = wp_list_pages( $page_list_args );
 
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.2/inc/shortcode-subpages.php /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.3/inc/shortcode-subpages.php
--- /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.2/inc/shortcode-subpages.php	2026-05-26 15:10:44.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.3/inc/shortcode-subpages.php	2026-05-29 03:09:30.000000000 +0000
@@ -27,8 +27,7 @@
 			'sort_order'   => $sort_order,
 			'link_before'  => esc_html($link_before),
 			'link_after'   => esc_html($link_after),
-			'post_type'    => $post_type,
-			'post_status'  => $post_status
+			'post_type'    => $post_type
 		);
 		$list_pages = wp_list_pages( $page_list_args );
 
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.2/page-list.php /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.3/page-list.php
--- /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.2/page-list.php	2026-05-26 15:10:44.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.3/page-list.php	2026-05-29 03:09:30.000000000 +0000
@@ -3,7 +3,7 @@
 Plugin Name: Page-list
 Plugin URI: http://wordpress.org/plugins/page-list/
 Description: [pagelist], [subpages], [siblings] and [pagelist_ext] shortcodes
-Version: 6.2
+Version: 6.3
 Author: webvitaly
 Author URI: http://web-profile.net/wordpress/plugins/
 License: GPLv3
@@ -11,7 +11,7 @@
 
 if ( ! defined( 'ABSPATH' ) ) { exit; }
 
-define('PAGE_LIST_PLUGIN_VERSION', '6.2');
+define('PAGE_LIST_PLUGIN_VERSION', '6.3');
 define('PAGE_LIST_PLUGIN_FILE', __FILE__);
 
 $pagelist_unq_settings = array(
@@ -36,7 +36,6 @@
 		'link_before' => '',
 		'link_after' => '',
 		'post_type' => 'page',
-		'post_status' => 'publish',
 		'class' => ''
 	)
 );
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.2/readme.txt /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.3/readme.txt
--- /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.2/readme.txt	2026-05-26 15:10:44.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/page-list/6.3/readme.txt	2026-05-29 03:09:30.000000000 +0000
@@ -4,7 +4,7 @@
 Tags: page-list, pagelist, sitemap, subpages, siblings
 Requires at least: 3.0
 Tested up to: 7.0
-Stable tag: 6.2
+Stable tag: 6.3
 License: GPLv3
 License URI: http://www.gnu.org/licenses/gpl.html
 
@@ -47,7 +47,6 @@
 * **number** - sets the number of pages to display: `[pagelist number="10"]`; by default the number is unlimited (number="");
 * **offset** - the number of pages to pass over (or displace) before collecting the set of pages: `[pagelist offset="5"]`; by default there is no offset (offset="");
 * **post_type** - list associated with a certain hierarchical Post Type `[pagelist post_type="page"]`; by default: (post_type="page"); possible values: page, revision, Hierarchical Custom Post Types ('post' is not a Hierarchical Post Type);
-* **post_status** - a comma-separated list of all post status types: `[pagelist post_status="private"]`; by default: (post_status="publish"); possible values: publish, private, draft;
 * **meta_key** and **meta_value** - only include the pages that have this Custom Field Key and this Custom Field Value: `[pagelist meta_key="metakey" meta_value="metaval"]`;
 * **show_date** - display creation or last modified date next to each Page: `[pagelist show_date="created"]`; possible values: created, modified, updated;
 * **date_format** - the format of the Page date set by the show_date parameter: `[pagelist date_format="l, F j, Y"]`; by default use the date format configured in your WordPress options;
@@ -83,7 +82,6 @@
 * **number** - sets the number of pages to display: `[pagelist_ext number="10"]`; by default the number is unlimited (number="");
 * **offset** - the number of pages to pass over (or displace) before collecting the set of pages: `[pagelist_ext offset="5"]`; by default there is no offset (offset="");
 * **post_type** - list associated with a certain hierarchical Post Type `[pagelist_ext post_type="page"]`; by default: (post_type="page"); possible values: page, revision, Hierarchical Custom Post Types ('post' is not a Hierarchical Post Type);
-* **post_status** - a comma-separated list of all post status types: `[pagelist_ext post_status="private"]`; by default: (post_status="publish"); possible values: publish, private, draft;
 * **class** - the CSS class for list of pages: `[pagelist_ext class="listclass"]`; by default the class is empty (class="");
 * **strip_tags** - strip tags or not: `[pagelist_ext strip_tags="0"]`; by default the tags are stripped (strip_tags="1");
 * **strip_shortcodes** - strip registered shortcodes or not: `[pagelist_ext strip_shortcodes="0"]`; by default shortcodes are stripped (strip_shortcodes="1") and all registered shortcodes are removed;
@@ -129,6 +127,9 @@
 
 == Changelog ==
 
+= 6.3 =
+* Removed post_status parameter from shortcodes and restricted meta key output to harden against unauthorized content disclosure
+
 = 6.2 =
 * Fixed issue with published pages not showing up in lists

Exploit Outline

The exploit is performed by an authenticated user with Contributor permissions or higher. The attacker creates a new post draft and inserts the [pagelist_ext] shortcode with the post_status attribute set to 'private' or 'draft'. Because the contributor's new post likely has no child pages, the plugin's internal logic falls back to a site-wide query. By previewing the post, the contributor can view the titles and full content of other users' private or draft posts in the rendering. Additionally, by using the show_meta_key attribute, the attacker can disclose internal or sensitive post metadata that would otherwise be hidden.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.