WordPress Core 6.3.2 Security Update – Technical Advisory

Published 13 October 2023
Updated 6 December 2023
Table of Contents

On the 12th of October 2023, WordPress.org released a security update and recommended users update their sites as soon as possible. This WordPress core 6.3.2 security release addresses 7 different security vulnerabilities and 1 potential security issue that affects multiple WordPress core versions.

For many, WordPress automatically updates the core to the latest version. Check if your WordPress version is 6.3.2 or any of the other patched versions – if not, update immediately!

Contributor+ Stored XSS via Navigation Link Block

Credits to our own security research team at Patchstack for discovering this vulnerability.

The underlying vulnerable code is located in the render_block_core_post_navigation_link function:

function render_block_core_post_navigation_link( $attributes, $content ) {
----------------- CUTTED HERE -----------------
	$arrow_map = array(
		'none'    => '',
		'arrow'   => array(
			'next'     => '→',
			'previous' => '←',
		),
		'chevron' => array(
			'next'     => '»',
			'previous' => '«',
		),
	);
----------------- CUTTED HERE -----------------
	// Display arrows.
	if ( isset( $attributes['arrow'] ) && ! empty( $attributes['arrow'] ) && 'none' !== $attributes['arrow'] ) {
		$arrow = $arrow_map[ $attributes['arrow'] ][ $navigation_type ];

		if ( 'next' === $navigation_type ) {
			$format = '%link <span class="wp-block-post-navigation-link__arrow-next is-arrow-' . $attributes['arrow'] . '" aria-hidden="true">' . $arrow . '</span>';
		} else {
			$format = '<span class="wp-block-post-navigation-link__arrow-previous is-arrow-' . $attributes['arrow'] . '" aria-hidden="true">' . $arrow . '</span> %link';
		}
	}

	// The dynamic portion of the function name, `$navigation_type`,
	// refers to the type of adjacency, 'next' or 'previous'.
	$get_link_function = "get_{$navigation_type}_post_link";
	$content           = $get_link_function( $format, $link );
	return sprintf(
		'<div %1$s>%2$s</div>',
		$wrapper_attributes,
		$content
	);
}

The function itself acts as a process handler of the wp:post-navigation-link block. The vulnerable process was located when the code tried to directly append $attributes['arrow'] value to $format which will eventually reflect as HTML via $content variable. Since there is no proper sanitization and escaping, we could escape the class attribute and perform a DOM-based XSS.

Potential impact

This allows authenticated users with a Contributor role and higher to inject a script into the WordPress page via the Navigation Link block and could result from stealing sensitive information to potentially privilege escalation on the WordPress site. Since the vulnerability is originally coming from the Gutenberg block, this issue also affected the stand-alone Gutenberg plugin.

The patch

Since the main issue is the lack of validation process on the $attributes['arrow'], implementing a check where the $attributes['arrow'] value should be a valid $arrow_map key value should fix the issue. The patch can be seen below:

Contributor+ Comment Read on Private and Password Protected Post

Credits to our security research team at Patchstack for discovering this vulnerability.

The main vulnerable feature exists on the overall WP-ADMIN Comments page. Generally, a user with a Contributor role can view all the comments made to Posts. The feature does not restrict read access to comments made to private and password-protected posts for the Contributor role.

Potential impact

This allows users with the minimum role of Contributor to read comments on the private and password-protected posts.

The patch

Since the main issue is there is no permission check on the comments page, applying a permission check should fix the issue. The patch can be seen below:

WordPress Core 6.3.2

Subscriber+ Arbitrary Shortcode Execution via parse-media-shortcode

Credits to John Blackbourn (WordPress Security Team), James GolovichJ.D GrimesNuman Turle, and WhiteCyberSec for discovering this vulnerability.

The underlying vulnerable code is located in the wp_ajax_parse_media_shortcode function:

function wp_ajax_parse_media_shortcode() {
	global $post, $wp_scripts;

	if ( empty( $_POST['shortcode'] ) ) {
		wp_send_json_error();
	}

	$shortcode = wp_unslash( $_POST['shortcode'] );

	if ( ! empty( $_POST['post_ID'] ) ) {
		$post = get_post( (int) $_POST['post_ID'] );
	}

	// The embed shortcode requires a post.
	if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) {
		if ( 'embed' === $shortcode ) {
			wp_send_json_error();
		}
	} else {
		setup_postdata( $post );
	}

	$parsed = do_shortcode( $shortcode );

	if ( empty( $parsed ) ) {
		wp_send_json_error(
			array(
				'type'    => 'no-items',
				'message' => __( 'No items found.' ),
			)
		);
	}

	$head   = '';
	$styles = wpview_media_sandbox_styles();

	foreach ( $styles as $style ) {
		$head .= '<link type="text/css" rel="stylesheet" href="' . $style . '">';
	}

	if ( ! empty( $wp_scripts ) ) {
		$wp_scripts->done = array();
	}

	ob_start();

	echo $parsed;

	if ( 'playlist' === $_REQUEST['type'] ) {
		wp_underscore_playlist_templates();

		wp_print_scripts( 'wp-playlist' );
	} else {
		wp_print_scripts( array( 'mediaelement-vimeo', 'wp-mediaelement' ) );
	}

	wp_send_json_success(
		array(
			'head' => $head,
			'body' => ob_get_clean(),
		)
	);
}

The function is used to handle parse-media-shortcode ajax action. Since there is no validation on the $shortcode variable and the code will process the variable directly to do_shortcode function, any authenticated user can execute an arbitrary shortcode.

Potential impact

This allows any authenticated user with a minimum Subscriber role and higher to execute an arbitrary shortcode. The actual impact of this vulnerability depends on the available shortcode on the site which can come from plugins or themes component.

The patch

Since the main issue is the lack of filtering on the executed media shortcode, implementing a whitelist check on the shortcode should fix the issue. The patch can be seen below:

WordPress Core 6.3.2

Reflected XSS via Application Password Requests

Credits to mascara7784 for discovering this vulnerability.

The underlying vulnerable code is located in the wp_is_authorize_application_password_request_valid function :

function wp_is_authorize_application_password_request_valid( $request, $user ) {
	$error    = new WP_Error();
	$is_local = 'local' === wp_get_environment_type();

	if ( ! empty( $request['success_url'] ) ) {
		$scheme = wp_parse_url( $request['success_url'], PHP_URL_SCHEME );

		if ( 'http' === $scheme && ! $is_local ) {
			$error->add(
				'invalid_redirect_scheme',
				__( 'The success URL must be served over a secure connection.' )
			);
		}
	}

	if ( ! empty( $request['reject_url'] ) ) {
		$scheme = wp_parse_url( $request['reject_url'], PHP_URL_SCHEME );

		if ( 'http' === $scheme && ! $is_local ) {
			$error->add(
				'invalid_redirect_scheme',
				__( 'The rejection URL must be served over a secure connection.' )
			);
		}
	}

	if ( ! empty( $request['app_id'] ) && ! wp_is_uuid( $request['app_id'] ) ) {
		$error->add(
			'invalid_app_id',
			__( 'The application ID must be a UUID.' )
		);
	}

	/**
	 * Fires before application password errors are returned.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_Error $error   The error object.
	 * @param array    $request The array of request data.
	 * @param WP_User  $user    The user authorizing the application.
	 */
	do_action( 'wp_authorize_application_password_request_errors', $error, $request, $user );

	if ( $error->has_errors() ) {
		return $error;
	}

	return true;
}

The function itself acts as a process handler to check if the Authorize Application Password request is valid. An application password itself is a password that we can create inside our User Profile for each WordPress website. If we give this password to another application, that application can use the password to authenticate to our WordPress site programmatically through the REST API.

The specific vulnerable variable exists on $request['success_url'] and $request['reject_url']. There is no proper validation on the given redirect URL value when the user decides to reject or approve an application password. This condition could be used to trigger reflected cross-site scripting by supplying the variable with a data: or javascript: protocol.

Potential impact

This allows unauthenticated users to inject a script into the WordPress page via the application password feature by tricking users into clicking to reject or approve an application password and could result from stealing sensitive information to potentially privilege escalation on the WordPress site.

The patch

Since the main issue is a lack of sanitization on the redirect URL value, implementing a protocol check fixes the issue. The patch can be seen below:

Contributor+ Stored XSS via Footnotes Block

Credits to Jorge Costa of the WordPress Core Team for discovering this vulnerability.

The origin of the vulnerable process is located in the render_block_core_footnotes function :

function render_block_core_footnotes( $attributes, $content, $block ) {
	// Bail out early if the post ID is not set for some reason.
	if ( empty( $block->context['postId'] ) ) {
		return '';
	}

	if ( post_password_required( $block->context['postId'] ) ) {
		return;
	}

	$footnotes = get_post_meta( $block->context['postId'], 'footnotes', true );

	if ( ! $footnotes ) {
		return;
	}

	$footnotes = json_decode( $footnotes, true );

	if ( ! is_array( $footnotes ) || count( $footnotes ) === 0 ) {
		return '';
	}

	$wrapper_attributes = get_block_wrapper_attributes();

	$block_content = '';

	foreach ( $footnotes as $footnote ) {
		$block_content .= sprintf(
			'<li id="%1$s">%2$s <a href="#%1$s-link">↩︎</a></li>',
			$footnote['id'],
			$footnote['content']
		);
	}

	return sprintf(
		'<ol %1$s>%2$s</ol>',
		$wrapper_attributes,
		$block_content
	);
}

This function will handle the process of footnotes block display. Notice that it will directly construct an HTML from the $footnote['content'] which come from the footnotes meta value of a post. The footnotes meta value itself could be set from the _wp_rest_api_autosave_meta function :

function _wp_rest_api_autosave_meta( $autosave ) {
	// Ensure it's a REST API request.
	if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
		return;
	}

	$body = rest_get_server()->get_raw_data();
	$body = json_decode( $body, true );

	if ( ! isset( $body['meta']['footnotes'] ) ) {
		return;
	}

	// `wp_creating_autosave` passes the array,
	// `_wp_put_post_revision` passes the ID.
	$id = is_int( $autosave ) ? $autosave : $autosave['ID'];

	if ( ! $id ) {
		return;
	}

	update_post_meta( $id, 'footnotes', wp_slash( $body['meta']['footnotes'] ) );
}

The function to store the footnotes meta doesn’t apply a proper filter and sanitization to the given value.

Potential impact

This allows authenticated users with a Contributor role and higher to inject a script into the WordPress page via the Footnotes block and could result from stealing sensitive information to potentially privilege escalation on the WordPress site. Since the vulnerability is originally coming from the Gutenberg block, this issue also affected the stand-alone Gutenberg plugin.

The patch

Since the main issue is the lack of sanitization process on the footnotes meta value, implementing a filter and sanitization with wp_filter_post_kses to the footnotes meta value should fix the issue. The patch can be seen below:

DoS via Cache Poisoning

Credits to s5s and raouf_maklouf for discovering this vulnerability.

The underlying vulnerable code is located in the serve_request function:

public function serve_request( $path = null ) {
    ----------------- CUTTED HERE -----------------

    /**
     * Filters whether to send nocache headers on a REST API request.
     *
     * @since 4.4.0
     *
     * @param bool $rest_send_nocache_headers Whether to send no-cache headers.
     */
    $send_no_cache_headers = apply_filters( 'rest_send_nocache_headers', is_user_logged_in() );
    if ( $send_no_cache_headers ) {
        foreach ( wp_get_nocache_headers() as $header => $header_value ) {
            if ( empty( $header_value ) ) {
                $this->remove_header( $header );
            } else {
                $this->send_header( $header, $header_value );
            }
        }
    }

    ----------------- CUTTED HERE -----------------

    $request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path );

    $request->set_query_params( wp_unslash( $_GET ) );
    $request->set_body_params( wp_unslash( $_POST ) );
    $request->set_file_params( $_FILES );
    $request->set_headers( $this->get_headers( wp_unslash( $_SERVER ) ) );
    $request->set_body( self::get_raw_data() );

    /*
     * HTTP method override for clients that can't use PUT/PATCH/DELETE. First, we check
     * $_GET['_method']. If that is not set, we check for the HTTP_X_HTTP_METHOD_OVERRIDE
     * header.
     */
    if ( isset( $_GET['_method'] ) ) {
        $request->set_method( $_GET['_method'] );
    } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
        $request->set_method( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] );
    }

    ----------------- CUTTED HERE -----------------

The function handles the serving of a REST API request. There is a case where an attacker could perform a DoS by supplying a X-HTTP-Method-Override header to a public API endpoint available on the site. In the case where the crafted request performed to the API returns a 4xx status code, other unauthenticated users will also face a 4xx status code when trying to access the API endpoint. The 4xx status code response could be caused by the restriction applied by the service to the given endpoint or the request method for the specific endpoint is not supported.

Potential impact

This allows unauthenticated users to deny access to the API endpoint via cache poisoning.

The patch

Since the main issue is a lack of proper validation checks, implementing a no cache for HTTP method override which causes a 4xx status code should fix the issue. The patch can be seen below:

Sensitive Information Exposure via User Search REST Endpoint

Credits to Marc Montpas of Automattic for discovering this vulnerability.

The underlying vulnerable code is located in the get_items function from WP_REST_Users_Controller class :

public function get_items( $request ) {

    // Retrieve the list of registered collection query parameters.
    $registered = $this->get_collection_params();

    /*
     * This array defines mappings between public API query parameters whose
     * values are accepted as-passed, and their internal WP_Query parameter
     * name equivalents (some are the same). Only values which are also
     * present in $registered will be set.
     */
    $parameter_mappings = array(
        'exclude'      => 'exclude',
        'include'      => 'include',
        'order'        => 'order',
        'per_page'     => 'number',
        'search'       => 'search',
        'roles'        => 'role__in',
        'capabilities' => 'capability__in',
        'slug'         => 'nicename__in',
    );

    $prepared_args = array();

    /*
     * For each known parameter which is both registered and present in the request,
     * set the parameter's value on the query $prepared_args.
     */
    foreach ( $parameter_mappings as $api_param => $wp_param ) {
        if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
            $prepared_args[ $wp_param ] = $request[ $api_param ];
        }
    }

    if ( isset( $registered['offset'] ) && ! empty( $request['offset'] ) ) {
        $prepared_args['offset'] = $request['offset'];
    } else {
        $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
    }

    if ( isset( $registered['orderby'] ) ) {
        $orderby_possibles        = array(
            'id'              => 'ID',
            'include'         => 'include',
            'name'            => 'display_name',
            'registered_date' => 'registered',
            'slug'            => 'user_nicename',
            'include_slugs'   => 'nicename__in',
            'email'           => 'user_email',
            'url'             => 'user_url',
        );
        $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
    }

    if ( isset( $registered['who'] ) && ! empty( $request['who'] ) && 'authors' === $request['who'] ) {
        $prepared_args['who'] = 'authors';
    } elseif ( ! current_user_can( 'list_users' ) ) {
        $prepared_args['has_published_posts'] = get_post_types( array( 'show_in_rest' => true ), 'names' );
    }

    if ( ! empty( $request['has_published_posts'] ) ) {
        $prepared_args['has_published_posts'] = ( true === $request['has_published_posts'] )
            ? get_post_types( array( 'show_in_rest' => true ), 'names' )
            : (array) $request['has_published_posts'];
    }

    if ( ! empty( $prepared_args['search'] ) ) {
        $prepared_args['search'] = '*' . $prepared_args['search'] . '*';
    }
    /**
     * Filters WP_User_Query arguments when querying users via the REST API.
     *
     * @link https://developer.wordpress.org/reference/classes/wp_user_query/
     *
     * @since 4.7.0
     *
     * @param array           $prepared_args Array of arguments for WP_User_Query.
     * @param WP_REST_Request $request       The REST API request.
     */
    $prepared_args = apply_filters( 'rest_user_query', $prepared_args, $request );

    $query = new WP_User_Query( $prepared_args );
    ----------------- CUTTED HERE -----------------

The function handles the process of searching for users on the WordPress site via the REST API endpoint. Since the search process could involve a user’s email address, unauthorized users, in this case, are able to perform brute force and gain verification regarding the user’s email address on the published posts or pages.

Potential impact

This allows unauthenticated users to perform a verification of existing users’ email addresses on the published posts or pages by only including the partial part of the targeted email.

The patch

Since the main issue is a lack of proper permission checks on the search process, restricting unauthorized users from including email addresses in the search process should fix the issue. The patch can be seen below:

Possible RCE Pop Chain Gadgets

Credits to Marc Montpas of Automattic for discovering this possible security issue.

In this security release, the core team has made a patch for possible RCE POP Chain Gadgets. These gadgets are observed to exist on several classes such as Requests/Session, Requests/Hooks, Request/Iri, WP_Theme, WP_Block_Type_Registry, and WP_Block_Patterns_Registry. POP Chain Gadgets itself could be very useful to maximize the impact of PHP Object Injection vulnerability.

Until this article is published, we still weren’t able to directly verify the exploitability of the possible gadgets and will continue to analyze the vulnerable classes and the patches.

Potential impact

This possibly could allow a PHP Object Injection (if it exists) vulnerability in the WordPress site to be escalated to a Remote Code Execution.

The patch

The core team made changes to prevent unintended behavior when certain objects are unserialized. The patch can be seen here.

Conclusion

In this article, we covered 7 medium severity vulnerabilities and 1 possible security issue that were patched in WordPress 6.3.2. Our research team also contributed to 2 out of 7 reported vulnerabilities.

See the official WordPress.org announcement: https://wordpress.org/news/2023/10/wordpress-6-3-2-maintenance-and-security-release/

Help us make the Internet a safer place

Making the WordPress ecosystem more secure is a team effort, and we believe that plugin developers and security researchers should work together.

  • If you’re a plugin developer, join our mVDP program that makes it easier to report, manage, and address vulnerabilities in your software.
  • If you’re a security researcher, join Patchstack Alliance to report vulnerabilities & earn rewards.

The latest in Security advisories

Looks like your browser is blocking our support chat widget. Turn off adblockers and reload the page.
crossmenu