Custom Query Blocks <= 5.5.0 - Authenticated (Contributor+) Stored Cross-Site Scripting
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:NTechnical Details
<=5.5.0What Changed in the Fix
Changes introduced in v5.6.0
Source Code
WordPress.org SVNh1>, <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.
*
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
@@ -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( @@ -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.