Headline
CVE-2023-0586: PostSettings.php in all-in-one-seo-pack/tags/4.2.9/app/Common/Admin – WordPress Plugin Repository
The All in One SEO Pack plugin for WordPress is vulnerable to Stored Cross-Site Scripting via multiple parameters in versions up to, and including, 4.2.9 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers with Contributor+ role to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
1<?php2namespace AIOSEO\Plugin\Common\Admin;34// Exit if accessed directly.5if ( ! defined( ‘ABSPATH’ ) ) {6 exit;7}89use AIOSEO\Plugin\Common\Models;1011/**12 * Abstract class that Pro and Lite both extend.13 *14 * @since 4.0.015 */16class PostSettings {17 /**18 * Initialize the admin.19 *20 * @since 4.0.021 *22 * @return void23 */24 public function __construct() {25 if ( defined( ‘DOING_AUTOSAVE’ ) && DOING_AUTOSAVE ) {26 return;27 }2829 // Clear the Post Type Overview cache.30 add_action( 'save_post’, [ $this, ‘clearPostTypeOverviewCache’ ], 100 );31 add_action( 'delete_post’, [ $this, ‘clearPostTypeOverviewCache’ ], 100 );32 add_action( 'wp_trash_post’, [ $this, ‘clearPostTypeOverviewCache’ ], 100 );3334 if ( wp_doing_ajax() || wp_doing_cron() || ! is_admin() ) {35 return;36 }3738 // Load Vue APP.39 add_action( 'admin_enqueue_scripts’, [ $this, ‘enqueuePostSettingsAssets’ ] );4041 // Add metabox.42 add_action( 'add_meta_boxes’, [ $this, ‘addPostSettingsMetabox’ ] );4344 // Add metabox to terms on init hook.45 add_action( 'init’, [ $this, ‘init’ ], 1000 );4647 // Save metabox.48 add_action( 'save_post’, [ $this, ‘saveSettingsMetabox’ ] );49 add_action( 'edit_attachment’, [ $this, ‘saveSettingsMetabox’ ] );50 add_action( 'add_attachment’, [ $this, ‘saveSettingsMetabox’ ] );5152 // Filter the sql clauses to show posts filtered by our params.53 add_filter( 'posts_clauses’, [ $this, ‘changeClausesToFilterPosts’ ], 10, 2 );54 }5556 /**57 * Enqueues the JS/CSS for the on page/posts settings.58 *59 * @since 4.0.060 *61 * @return void62 */63 public function enqueuePostSettingsAssets() {64 if (65 aioseo()->helpers->isScreenBase( ‘event-espresso’ ) ||66 aioseo()->helpers->isScreenBase( ‘post’ ) ||67 aioseo()->helpers->isScreenBase( ‘term’ ) ||68 aioseo()->helpers->isScreenBase( ‘edit-tags’ ) ||69 aioseo()->helpers->isScreenBase( ‘site-editor’ )70 ) {71 $page = null;72 if (73 aioseo()->helpers->isScreenBase( ‘event-espresso’ ) ||74 aioseo()->helpers->isScreenBase( ‘post’ )75 ) {76 $page = 'post’;77 }7879 aioseo()->core->assets->load( 'src/vue/standalone/post-settings/main.js’, [], aioseo()->helpers->getVueData( $page ) );80 aioseo()->core->assets->load( 'src/vue/standalone/link-format/main.js’, [], aioseo()->helpers->getVueData( $page ) );81 aioseo()->admin->enqueueAioseoModalPortal();82 }8384 $screen = get_current_screen();85 if ( ‘attachment’ === $screen->id ) {86 wp_enqueue_media();87 }88 }8990 /**91 * Check whether or not we can add the metabox.92 *93 * @since 4.1.794 *95 * @param string $postType The post type to check.96 * @return boolean Whether or not can add the Metabox.97 */98 public function canAddPostSettingsMetabox( $postType ) {99 $dynamicOptions = aioseo()->dynamicOptions->noConflict();100101 $pageAnalysisSettingsCapability = aioseo()->access->hasCapability( ‘aioseo_page_analysis’ );102 $generalSettingsCapability = aioseo()->access->hasCapability( ‘aioseo_page_general_settings’ );103 $socialSettingsCapability = aioseo()->access->hasCapability( ‘aioseo_page_social_settings’ );104 $schemaSettingsCapability = aioseo()->access->hasCapability( ‘aioseo_page_schema_settings’ );105 $linkAssistantCapability = aioseo()->access->hasCapability( ‘aioseo_page_link_assistant_settings’ );106 $redirectsCapability = aioseo()->access->hasCapability( ‘aioseo_page_redirects_manage’ );107 $advancedSettingsCapability = aioseo()->access->hasCapability( ‘aioseo_page_advanced_settings’ );108109 if (110 $dynamicOptions->searchAppearance->postTypes->has( $postType ) &&111 $dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox &&112 ! (113 empty( $pageAnalysisSettingsCapability ) &&114 empty( $generalSettingsCapability ) &&115 empty( $socialSettingsCapability ) &&116 empty( $schemaSettingsCapability ) &&117 empty( $linkAssistantCapability ) &&118 empty( $redirectsCapability ) &&119 empty( $advancedSettingsCapability )120 )121 ) {122 return true;123 }124125 return false;126 }127128 /**129 * Adds a meta box to page/posts screens.130 *131 * @since 4.0.0132 *133 * @return void134 */135 public function addPostSettingsMetabox() {136 $screen = get_current_screen();137 $postType = $screen->post_type;138139 if ( $this->canAddPostSettingsMetabox( $postType ) ) {140 // Translators: 1 - The plugin short name (“AIOSEO”).141 $aioseoMetaboxTitle = sprintf( esc_html__( '%1$s Settings’, ‘all-in-one-seo-pack’ ), AIOSEO_PLUGIN_SHORT_NAME );142143 add_meta_box(144 'aioseo-settings’,145 $aioseoMetaboxTitle,146 [ $this, ‘postSettingsMetabox’ ],147 [ $postType ],148 'normal’,149 apply_filters( 'aioseo_post_metabox_priority’, ‘high’ )150 );151 }152 }153154 /**155 * Render the on page/posts settings metabox with Vue App wrapper.156 *157 * @since 4.0.0158 *159 * @param WP_Post $post The current post.160 * @return void161 */162 public function postSettingsMetabox() {163 $this->postSettingsHiddenField();164 ?>165 <div id="aioseo-post-settings-metabox">166 <?php aioseo()->templates->getTemplate( ‘parts/loader.php’ ); ?>167 </div>168 <?php169 }170171 /**172 * Adds the hidden field where all the metabox data goes.173 *174 * @since 4.0.17175 *176 * @return void177 */178 public function postSettingsHiddenField() {179 static $fieldExists = false;180 if ( $fieldExists ) {181 return;182 }183184 $fieldExists = true;185186 ?>187 <div id="aioseo-post-settings-field">188 <input type="hidden" name="aioseo-post-settings" id="aioseo-post-settings" value=""/>189 <?php wp_nonce_field( 'aioseoPostSettingsNonce’, ‘PostSettingsNonce’ ); ?>190 </div>191 <?php192 }193194 /**195 * Handles metabox saving.196 *197 * @since 4.0.3198 *199 * @param int $postId Post ID.200 * @return void201 */202 public function saveSettingsMetabox( $postId ) {203 if ( ! aioseo()->helpers->isValidPost( $postId, [ ‘all’ ] ) ) {204 return;205 }206207 // Security check.208 if ( ! isset( $_POST[‘PostSettingsNonce’] ) || ! wp_verify_nonce( $_POST[‘PostSettingsNonce’], ‘aioseoPostSettingsNonce’ ) ) {209 return;210 }211212 // If we don’t have our post settings input, we can safely skip.213 if ( ! isset( $_POST[‘aioseo-post-settings’] ) ) {214 return;215 }216217 // Check user permissions.218 if ( ! current_user_can( 'edit_post’, $postId ) ) {219 return;220 }221222 $currentPost = json_decode( stripslashes( $_POST[‘aioseo-post-settings’] ), true ); // phpcs:ignore HM.Security.ValidatedSanitizedInput223224 // If there is no data, there likely was an error, e.g. if the hidden field wasn’t populated on load and the user saved the post without making changes in the metabox.225 // In that case we should return to prevent a complete reset of the data.226 if ( empty( $currentPost ) ) {227 return;228 }229230 Models\Post::savePost( $postId, $currentPost );231 }232233 /**234 * Clear the Post Type Overview cache from our cache table.235 *236 * @since 4.2.0237 *238 * @param int $postId The Post ID being updated/deleted.239 * @return void240 */241 public function clearPostTypeOverviewCache( $postId ) {242 $postType = get_post_type( $postId );243 if ( empty( $postType ) ) {244 return;245 }246247 aioseo()->core->cache->delete( $postType . ‘_overview_data’ );248 }249250 /**251 * Get a list of post types with an overview showing how many posts are good, okay and so on.252 *253 * @since 4.2.0254 *255 * @return array The list of post types with the overview.256 */257 public function getPostTypesOverview() {258 $postTypes = [];259 $dynamicOptions = aioseo()->dynamicOptions->noConflict();260261 foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) {262 if (263 ! $dynamicOptions->searchAppearance->postTypes->has( $postType ) ||264 ! $dynamicOptions->searchAppearance->postTypes->$postType->show ||265 ! $dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox ||266 ‘attachment’ === $postType ||267 aioseo()->helpers->isBBPressPostType( $postType )268 ) {269 continue;270 }271272 $postTypes[ $postType ] = $this->getPostTypeOverview( $postType );273 }274275 return $postTypes;276 }277278 /**279 * Get how many posts are good, okay, needs improvement or are missing the focus keyphrase for the given post type.280 *281 * @since 4.2.0282 *283 * @param string $postType The post type name.284 * @return array The overview for the given post type.285 */286 public function getPostTypeOverview( $postType ) {287 $overview = aioseo()->core->cache->get( $postType . ‘_overview_data’ );288 if ( null !== $overview ) {289 return $overview;290 }291292 $posts = aioseo()->core->db->start( ‘posts as p’ )293 ->select( ‘ap.seo_score, ap.keyphrases’ )294 ->leftJoin( 'aioseo_posts as ap’, ‘ap.post_id = p.ID’ )295 ->where( 'p.post_status’, ‘publish’ )296 ->where( 'p.post_type’, $postType )297 ->run()298 ->result();299300 $overview = [301 ‘total’ => count( $posts ),302 ‘needsImprovement’ => 0,303 ‘okay’ => 0,304 ‘good’ => 0,305 ‘withoutFocusKeyphrase’ => 0,306 ];307308 foreach ( $posts as $post ) {309 if ( empty( $post->keyphrases ) || strpos( $post->keyphrases, '{"focus":[]' ) === 0 ) {310 $overview[‘withoutFocusKeyphrase’]++;311312 // We skip to the next since we will just consider posts with focus keyphrase in the counts.313 continue;314 }315316 if ( 50 > $post->seo_score ) {317 $overview[‘needsImprovement’]++;318 continue;319 }320321 if ( 50 <= $post->seo_score && 80 >= $post->seo_score ) {322 $overview[‘okay’]++;323 continue;324 }325326 if ( 80 < $post->seo_score ) {327 $overview[‘good’]++;328 }329 }330331 aioseo()->core->cache->update( $postType . '_overview_data’, $overview, WEEK_IN_SECONDS );332333 return $overview;334 }335336 /**337 * Change the JOIN and WHERE clause to filter just the posts we need to show depending on the query string.338 *339 * @since 4.2.0340 *341 * @param array $clauses Associative array of the clauses for the query.342 * @param WP_Query $query The WP_Query instance (passed by reference).343 * @return array The clauses array updated.344 */345 public function changeClausesToFilterPosts( $clauses, $query ) {346 if ( ! is_admin() || ! $query->is_main_query() ) {347 return $clauses;348 }349350 $filter = filter_input( INPUT_GET, ‘aioseo-filter’ );351 if ( empty( $filter ) ) {352 return $clauses;353 }354355 $whereClause = '’;356 $noKeyphrasesClause = " (aioseo_p.keyphrases = ‘’ OR aioseo_p.keyphrases IS NULL OR aioseo_p.keyphrases LIKE ‘{\"focus\":[]%’) ";357 switch ( $filter ) {358 case 'withoutFocusKeyphrase’:359 $whereClause = " AND $noKeyphrasesClause ";360 break;361 case 'needsImprovement’:362 $whereClause = " AND aioseo_p.seo_score < 50 AND NOT $noKeyphrasesClause ";363 break;364 case 'okay’:365 $whereClause = " AND aioseo_p.seo_score BETWEEN 50 AND 80 AND NOT $noKeyphrasesClause ";366 break;367 case 'good’:368 $whereClause = " AND aioseo_p.seo_score > 80 AND NOT $noKeyphrasesClause ";369 break;370 }371372 $prefix = aioseo()->core->db->prefix;373 $postsTable = aioseo()->core->db->db->posts;374 $clauses[‘join’] .= " LEFT JOIN {$prefix}aioseo_posts AS aioseo_p ON ({$postsTable}.ID = aioseo_p.post_id) ";375 $clauses[‘where’] .= $whereClause;376377 return $clauses;378 }379}