Skip to content

Commit 553f97b

Browse files
fix(security): escape Stripe field block output
The Stripe field block echoed the id/mappedName/fieldOptionName block attributes and the Stripe product image/description/name into HTML with no escaping. Escape the product sinks (esc_url/esc_attr/esc_html) and extract the field attributes into get_field_attributes() with esc_attr. Adds Test_Render_Escaping covering the review-comparison, plugin-card and Stripe field escaping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 99716bd commit 553f97b

2 files changed

Lines changed: 121 additions & 5 deletions

File tree

plugins/otter-pro/inc/render/class-form-stripe-block.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function render( $attributes ) {
5050
$details_markup = '';
5151

5252
if ( 0 < count( $product['images'] ) ) {
53-
$details_markup .= '<img src="' . $product['images'][0] . '" alt="' . $product['description'] . '" />';
53+
$details_markup .= '<img src="' . esc_url( $product['images'][0] ) . '" alt="' . esc_attr( $product['description'] ) . '" />';
5454
}
5555

5656
$price = $stripe->create_request( 'price', $attributes['price'] );
@@ -67,13 +67,11 @@ public function render( $attributes ) {
6767
$amount = number_format( $price['unit_amount'] / 100, 2, '.', ' ' );
6868

6969
$details_markup .= '<div class="o-stripe-checkout-description">';
70-
$details_markup .= '<h3>' . $product['name'] . '</h3>';
70+
$details_markup .= '<h3>' . esc_html( $product['name'] ) . '</h3>';
7171
$details_markup .= '<h5>' . $currency . $amount . '</h5>';
7272
$details_markup .= '</div>';
7373

74-
$html_attributes = 'id="' . $attributes['id'] . '" ' .
75-
( isset( $attributes['mappedName'] ) ? ( ' name="' . $attributes['mappedName'] . '"' ) : '' ) .
76-
( isset( $attributes['fieldOptionName'] ) ? ( ' data-field-option-name="' . $attributes['fieldOptionName'] . '"' ) : '' );
74+
$html_attributes = $this->get_field_attributes( $attributes );
7775

7876
return sprintf(
7977
'<div %1$s><div class="o-stripe-checkout">%2$s</div><div class="o-payment-element"></div></div>',
@@ -82,6 +80,18 @@ public function render( $attributes ) {
8280
);
8381
}
8482

83+
/**
84+
* Build the field HTML attributes (id/name/data-field-option-name) from block attributes.
85+
*
86+
* @param array<string, mixed> $attributes Block attributes.
87+
* @return string
88+
*/
89+
public function get_field_attributes( $attributes ) {
90+
return 'id="' . esc_attr( $attributes['id'] ) . '" ' .
91+
( isset( $attributes['mappedName'] ) ? ( ' name="' . esc_attr( $attributes['mappedName'] ) . '"' ) : '' ) .
92+
( isset( $attributes['fieldOptionName'] ) ? ( ' data-field-option-name="' . esc_attr( $attributes['fieldOptionName'] ) . '"' ) : '' );
93+
}
94+
8595
/**
8696
* Format the error message.
8797
*

tests/test-render-escaping.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
/**
3+
* Output-escaping regression tests for block render callbacks.
4+
*
5+
* @package gutenberg-blocks
6+
*/
7+
8+
use ThemeIsle\GutenbergBlocks\Render\Plugin_Card_Block;
9+
use ThemeIsle\OtterPro\Render\Review_Comparison_Block;
10+
use ThemeIsle\OtterPro\Render\Form_Stripe_Block;
11+
12+
/**
13+
* Render escaping test case.
14+
*/
15+
class Test_Render_Escaping extends WP_UnitTestCase {
16+
17+
/**
18+
* Provide a block-render context so get_block_wrapper_attributes() works when
19+
* render callbacks are invoked directly.
20+
*/
21+
public function set_up() {
22+
parent::set_up();
23+
\WP_Block_Supports::$block_to_render = array(
24+
'blockName' => '',
25+
'attrs' => array(),
26+
);
27+
}
28+
29+
/**
30+
* Tear down the test.
31+
*/
32+
public function tear_down() {
33+
\WP_Block_Supports::$block_to_render = null;
34+
parent::tear_down();
35+
}
36+
37+
/**
38+
* Review comparison must escape values pulled from the referenced review block.
39+
*/
40+
public function test_review_comparison_escapes_referenced_block_attributes() {
41+
$block_markup = '<!-- wp:themeisle-blocks/review {"id":"aaaaaaaabbbb1234","title":"<script>alert(1)</script>","price":"9.99","description":"<img src=x onerror=alert(2)>","features":[{"title":"<script>alert(3)</script>","rating":8}]} /-->';
42+
43+
// Store the block markup raw (bypass save-time kses) so the test exercises the
44+
// render-layer escaping, not WordPress's save-time sanitization.
45+
kses_remove_filters();
46+
$post_id = $this->factory()->post->create(
47+
array(
48+
'post_content' => $block_markup,
49+
'post_status' => 'publish',
50+
)
51+
);
52+
kses_init_filters();
53+
54+
$html = ( new Review_Comparison_Block() )->render(
55+
array( 'reviews' => array( $post_id . '-bbbb1234' ) )
56+
);
57+
58+
$this->assertStringNotContainsString( '<script>alert(1)</script>', $html );
59+
$this->assertStringNotContainsString( '<script>alert(3)</script>', $html );
60+
$this->assertStringNotContainsString( 'onerror=', $html );
61+
}
62+
63+
/**
64+
* The plugin card must escape the author field from the wordpress.org API.
65+
*/
66+
public function test_plugin_card_escapes_author() {
67+
$slug = 'otter-blocks';
68+
69+
$results = new stdClass();
70+
$results->name = 'Otter';
71+
$results->author = '<script>alert(1)</script>';
72+
$results->version = '1.0';
73+
$results->rating = 90;
74+
$results->num_ratings = 10;
75+
$results->active_installs = 1000;
76+
$results->requires = '6.0';
77+
$results->tested = '6.4';
78+
$results->homepage = 'https://example.com';
79+
$results->download_link = 'https://example.com/plugin.zip';
80+
$results->short_description = 'A plugin.';
81+
$results->icons = array( 'default' => 'https://example.com/icon.png' );
82+
83+
set_transient( 'otter_plugin_card_' . sanitize_key( $slug ), $results, HOUR_IN_SECONDS );
84+
85+
$html = ( new Plugin_Card_Block() )->render( array( 'slug' => $slug ) );
86+
87+
delete_transient( 'otter_plugin_card_' . sanitize_key( $slug ) );
88+
89+
$this->assertStringNotContainsString( '<script>alert(1)</script>', $html );
90+
}
91+
92+
/**
93+
* The Stripe field block must escape id/name/option-name block attributes.
94+
*/
95+
public function test_form_stripe_field_attributes_are_escaped() {
96+
$out = ( new Form_Stripe_Block() )->get_field_attributes(
97+
array(
98+
'id' => 'x"><script>alert(1)</script>',
99+
'mappedName' => 'n"><script>alert(2)</script>',
100+
'fieldOptionName' => 'f"><script>alert(3)</script>',
101+
)
102+
);
103+
104+
$this->assertStringNotContainsString( '<script>', $out );
105+
}
106+
}

0 commit comments

Comments
 (0)