CVE-2026-39575

Custom Query Blocks <= 5.5.0 - Authenticated (Contributor+) Stored Cross-Site Scripting

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
5.6.0
Patched in
63d
Time to patch

Description

The Custom Query Blocks plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 5.5.0 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with contributor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=5.5.0
PublishedFebruary 12, 2026
Last updatedApril 15, 2026

What Changed in the Fix

Changes introduced in v5.6.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

h1>, <h2>, etc.). If `custom_posts` does: ```php $title_tag = $attributes['titleTag']; $markup .= "<$title_tag>" . get_the_title() . "</$title_tag>"; ``` And `title_tag` is only sanitized with `sanitize_text_field` or `esc_html`…

Show full research plan

h1>,

, etc.).
If custom_posts does:
php $title_tag = $attributes['titleTag']; $markup .= "<$title_tag>" . get_the_title() . "</$title_tag>";
And title_tag is only sanitized with sanitize_text_field or esc_html.
esc_html("h1 onmouseover=alert(1)") -> h1 onmouseover=alert(1).
This would result in: <h1 onmouseover=alert(1)>Title</h1>. BINGO.

        Let's look at the `custom_posts` initialization again.
        ```php
        $attributes['displayTitle']        = isset( $attributes['displayTitle'] ) ? esc_html( $attributes['displayTitle'] ) : true;
        ```
        Wait, if `displayTitle` is used as the tag name? No, that doesn't make sense.
        But the changelog *explicitly* says `title tag`.

        Let's assume the block name is `ptam/custom-post-types`.
        Let's assume there's an attribute `titleTag`.

        Wait, look at `readme.txt` again.
        `* Enhancement/fix: Blocks are now compatible with Block.json v3.`
        This confirms it's a Gutenberg block.

*   Contributor role.
*   

Research Findings
Static analysis — not yet PoC-verified

Summary

The Custom Query Blocks plugin is vulnerable to Stored Cross-Site Scripting (XSS) via the 'imageAlignment' attribute in Gutenberg blocks. Authenticated attackers with contributor-level permissions can inject malicious JavaScript into the 'style' attribute of post grid elements, which executes in the browser of any user viewing the affected page.

Vulnerable Code

// includes/blocks/custom-post-types/class-custom-post-types.php (approx lines 80-84 and 94-98)
$list_item_markup .= sprintf(
    '<div class="ptam-block-post-grid-image" %3$s><a href="%1$s" rel="bookmark">%2$s</a></div>',
    esc_url( get_permalink( $post_id ) ),
    get_avatar( $post_author, $attributes['avatarSize'] ),
    'grid' === $attributes['postLayout'] ? "style='text-align: {$image_alignment}'" : ''
);

---

// includes/blocks/custom-post-types/class-custom-post-types.php (line 441-444, inferred from patch)
$list_items_markup .= sprintf(
    '<div class="ptam-block-post-grid-image" %3$s><a href="%1$s" rel="bookmark">%2$s</a></div>',
    esc_url( get_permalink( $post_id ) ),
    $this->get_profile_image( $attributes, $post_thumb_id, $post->post_author, $post->ID ),
    'grid' === $attributes['postLayout'] ? "style='text-align: {$attributes['imageAlignment']}'" : ''
);

---

// includes/class-functions.php (lines 50-55)
public static function sanitize_attribute( $attributes, $attribute, $type = 'text' ) {
    if ( isset( $attributes[ $attribute ] ) ) {
        switch ( $type ) {
            case 'text':
                return sanitize_text_field( $attributes[ $attribute ] );

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/post-type-archive-mapping/5.5.0/includes/blocks/custom-post-types/class-custom-post-types.php /home/deploy/wp-safety.org/data/plugin-versions/post-type-archive-mapping/5.6.0/includes/blocks/custom-post-types/class-custom-post-types.php
--- /home/deploy/wp-safety.org/data/plugin-versions/post-type-archive-mapping/5.5.0/includes/blocks/custom-post-types/class-custom-post-types.php	2026-02-20 04:43:08.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/post-type-archive-mapping/5.6.0/includes/blocks/custom-post-types/class-custom-post-types.php	2026-03-06 22:17:08.000000000 +0000
@@ -66,7 +66,7 @@
 			'center',
 			'right',
 			);
-		$image_alignment          = Functions::sanitize_attribute( $attributes, 'imageAlignment', 'text' );
+		$image_alignment          = Functions::sanitize_attribute( $attributes, 'imageAlignment', 'attr' );
 		if ( ! in_array( $image_alignment, $image_alignments_options, true ) ) {
 			$image_alignment = 'left';
 		}
@@ -80,7 +80,7 @@
 						'<div class="ptam-block-post-grid-image" %3$s><a href="%1$s" rel="bookmark">%2$s</a></div>',
 						esc_url( get_permalink( $post_id ) ),
 						get_avatar( $post_author, $attributes['avatarSize'] ),
-						'grid' === $attributes['postLayout'] ? "style='text-align: {$image_alignment}'" : ''
+						'grid' === $attributes['postLayout'] ? "style='text-align: " . esc_attr( $image_alignment ) . "'" : ''
 					);
 				} else {
 					$list_item_markup .= sprintf(
@@ -94,7 +94,7 @@
 						'<div class="ptam-block-post-grid-image" %3$s><a href="%1$s" rel="bookmark">%2$s</a></div>',
 						esc_url( get_permalink( $post_id ) ),
 						wp_get_attachment_image( $post_thumb_id, $post_thumb_size ),
-						'grid' === $attributes['postLayout'] ? "style='text-align: {$image_alignment}'" : ''
+						'grid' === $attributes['postLayout'] ? "style='text-align: " . esc_attr( $image_alignment ) . "'" : ''
 					);
 			} else {
 				$list_item_markup .= sprintf(
@@ -441,7 +441,7 @@
 							'<div class="ptam-block-post-grid-image" %3$s><a href="%1$s" rel="bookmark">%2$s</a></div>',
 							esc_url( get_permalink( $post_id ) ),
 							$this->get_profile_image( $attributes, $post_thumb_id, $post->post_author, $post->ID ),
-							'grid' === $attributes['postLayout'] ? "style='text-align: {$attributes['imageAlignment']}'" : ''
+							'grid' === $attributes['postLayout'] ? "style='text-align: " . esc_attr( $attributes['imageAlignment'] ) . "'" : ''
 						);
 					} else {
 						$list_items_markup .= sprintf(
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/post-type-archive-mapping/5.5.0/includes/class-functions.php /home/deploy/wp-safety.org/data/plugin-versions/post-type-archive-mapping/5.6.0/includes/class-functions.php
--- /home/deploy/wp-safety.org/data/plugin-versions/post-type-archive-mapping/5.5.0/includes/class-functions.php	2021-03-25 13:40:36.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/post-type-archive-mapping/5.6.0/includes/class-functions.php	2026-03-06 22:17:08.000000000 +0000
@@ -50,6 +50,8 @@
 	public static function sanitize_attribute( $attributes, $attribute, $type = 'text' ) {
 		if ( isset( $attributes[ $attribute ] ) ) {
 			switch ( $type ) {
+				case 'attr':
+					return esc_attr( $attributes[ $attribute ] );
 				case 'text':
 					return sanitize_text_field( $attributes[ $attribute ] );
 				case 'bool':

Exploit Outline

The exploit requires Contributor-level authentication. An attacker can create or edit a post and add a Custom Post Types block. By modifying the block's attributes via the editor (or manually crafting the block comment in the post content), the attacker sets the 'imageAlignment' attribute to a payload that breaks out of an HTML attribute, such as `left' onmouseover='alert(1)`. The 'postLayout' attribute must be set to 'grid'. When a user views the post, the unescaped 'imageAlignment' attribute is rendered directly into a 'style' attribute, triggering the execution of the injected JavaScript.

Check if your site is affected.

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