Commit 73df05da2c5 for woocommerce
commit 73df05da2c5dc8fb593183a736f137cbd98b5e34
Author: Darren Ethier <darren@roughsmootheng.in>
Date: Mon Aug 31 17:03:58 2026 -0400
Escape settings attributes and report currency symbols (#68209)
diff --git a/plugins/woocommerce/changelog/fix-woo6-119-context-specific-output-escaping b/plugins/woocommerce/changelog/fix-woo6-119-context-specific-output-escaping
new file mode 100644
index 00000000000..5ae563e6f87
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-woo6-119-context-specific-output-escaping
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Escape settings field attributes and currency symbols in legacy report scripts.
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php
index 5e3245818fd..32a96325dfc 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php
@@ -709,7 +709,10 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
<label><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Sanitized, filterable help-tip markup from get_field_description(). ?></label>
</th>
<td class="forminp">
- <?php echo str_replace( ' id=', " data-placeholder='" . esc_attr__( 'Select a page…', 'woocommerce' ) . "' style='" . $value['css'] . "' class='" . $value['class'] . "' id=", wp_dropdown_pages( $args ) ); // WPCS: XSS ok. ?> <?php echo $description; // WPCS: XSS ok. ?>
+ <?php
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_dropdown_pages() and get_field_description() return intentional, sanitized markup; injected attributes are escaped here.
+ echo str_replace( ' id=', " data-placeholder='" . esc_attr__( 'Select a page…', 'woocommerce' ) . "' style='" . esc_attr( $value['css'] ) . "' class='" . esc_attr( $value['class'] ) . "' id=", wp_dropdown_pages( $args ) ) . ' ' . $description;
+ ?>
</td>
</tr>
<?php
diff --git a/plugins/woocommerce/includes/admin/reports/class-wc-admin-report.php b/plugins/woocommerce/includes/admin/reports/class-wc-admin-report.php
index 04cde823bb9..8b1765a031e 100644
--- a/plugins/woocommerce/includes/admin/reports/class-wc-admin-report.php
+++ b/plugins/woocommerce/includes/admin/reports/class-wc-admin-report.php
@@ -774,19 +774,21 @@ class WC_Admin_Report {
* @return string
*/
public function get_currency_tooltip() {
+ $currency_symbol = get_woocommerce_currency_symbol();
+
switch ( get_option( 'woocommerce_currency_pos' ) ) {
case 'right':
- $currency_tooltip = 'append_tooltip: "' . get_woocommerce_currency_symbol() . '"';
+ $currency_tooltip = 'append_tooltip: ' . wp_json_encode( $currency_symbol, JSON_UNESCAPED_UNICODE );
break;
case 'right_space':
- $currency_tooltip = 'append_tooltip: " ' . get_woocommerce_currency_symbol() . '"';
+ $currency_tooltip = 'append_tooltip: ' . wp_json_encode( ' ' . $currency_symbol, JSON_UNESCAPED_UNICODE );
break;
case 'left':
- $currency_tooltip = 'prepend_tooltip: "' . get_woocommerce_currency_symbol() . '"';
+ $currency_tooltip = 'prepend_tooltip: ' . wp_json_encode( $currency_symbol, JSON_UNESCAPED_UNICODE );
break;
case 'left_space':
default:
- $currency_tooltip = 'prepend_tooltip: "' . get_woocommerce_currency_symbol() . ' "';
+ $currency_tooltip = 'prepend_tooltip: ' . wp_json_encode( $currency_symbol . ' ', JSON_UNESCAPED_UNICODE );
break;
}
diff --git a/plugins/woocommerce/includes/admin/reports/class-wc-report-sales-by-date.php b/plugins/woocommerce/includes/admin/reports/class-wc-report-sales-by-date.php
index a6df6ac8f9c..33420d2403f 100644
--- a/plugins/woocommerce/includes/admin/reports/class-wc-report-sales-by-date.php
+++ b/plugins/woocommerce/includes/admin/reports/class-wc-report-sales-by-date.php
@@ -747,7 +747,7 @@ class WC_Report_Sales_By_Date extends WC_Admin_Report {
points: { show: true, radius: 5, lineWidth: 2, fillColor: '#fff', fill: true },
lines: { show: true, lineWidth: 2, fill: false },
shadowSize: 0,
- <?php echo $this->get_currency_tooltip(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
+ <?php echo $this->get_currency_tooltip(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Returns a fixed JavaScript property with a JSON-encoded currency symbol. ?>
},
{
label: "<?php echo esc_js( __( 'Shipping amount', 'woocommerce' ) ); ?>",
@@ -757,7 +757,7 @@ class WC_Report_Sales_By_Date extends WC_Admin_Report {
points: { show: true, radius: 5, lineWidth: 2, fillColor: '#fff', fill: true },
lines: { show: true, lineWidth: 2, fill: false },
shadowSize: 0,
- prepend_tooltip: "<?php echo get_woocommerce_currency_symbol(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>"
+ prepend_tooltip: <?php echo wp_json_encode( get_woocommerce_currency_symbol(), JSON_UNESCAPED_UNICODE ); ?>
},
{
label: "<?php echo esc_js( __( 'Gross sales amount', 'woocommerce' ) ); ?>",
@@ -767,7 +767,7 @@ class WC_Report_Sales_By_Date extends WC_Admin_Report {
points: { show: true, radius: 5, lineWidth: 2, fillColor: '#fff', fill: true },
lines: { show: true, lineWidth: 2, fill: false },
shadowSize: 0,
- <?php echo $this->get_currency_tooltip(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
+ <?php echo $this->get_currency_tooltip(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Returns a fixed JavaScript property with a JSON-encoded currency symbol. ?>
},
{
label: "<?php echo esc_js( __( 'Net sales amount', 'woocommerce' ) ); ?>",
@@ -777,7 +777,7 @@ class WC_Report_Sales_By_Date extends WC_Admin_Report {
points: { show: true, radius: 6, lineWidth: 4, fillColor: '#fff', fill: true },
lines: { show: true, lineWidth: 5, fill: false },
shadowSize: 0,
- <?php echo $this->get_currency_tooltip(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
+ <?php echo $this->get_currency_tooltip(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Returns a fixed JavaScript property with a JSON-encoded currency symbol. ?>
},
{
label: "<?php echo esc_js( __( 'Refund amount', 'woocommerce' ) ); ?>",
@@ -787,7 +787,7 @@ class WC_Report_Sales_By_Date extends WC_Admin_Report {
points: { show: true, radius: 5, lineWidth: 2, fillColor: '#fff', fill: true },
lines: { show: true, lineWidth: 2, fill: false },
shadowSize: 0,
- prepend_tooltip: "<?php echo get_woocommerce_currency_symbol(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>"
+ prepend_tooltip: <?php echo wp_json_encode( get_woocommerce_currency_symbol(), JSON_UNESCAPED_UNICODE ); ?>
},
];
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/admin/reports/class-wc-tests-admin-report.php b/plugins/woocommerce/tests/legacy/unit-tests/admin/reports/class-wc-tests-admin-report.php
index 13fc6861834..af7272f6f82 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/admin/reports/class-wc-tests-admin-report.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/admin/reports/class-wc-tests-admin-report.php
@@ -168,4 +168,41 @@ class WC_Tests_Admin_Report extends WC_Unit_Test_Case {
$this->assertEquals( $product->get_name(), $data->name );
}
+
+ /**
+ * @testdox Currency tooltip fragments preserve filtered symbols as valid JavaScript strings.
+ */
+ public function test_get_currency_tooltip_encodes_filtered_symbol(): void {
+ $currency_symbol = "'\"\\</script>€雪";
+ $currency_pos = get_option( 'woocommerce_currency_pos', null );
+ $positions = array(
+ 'right' => array( 'append_tooltip', $currency_symbol ),
+ 'right_space' => array( 'append_tooltip', ' ' . $currency_symbol ),
+ 'left' => array( 'prepend_tooltip', $currency_symbol ),
+ 'left_space' => array( 'prepend_tooltip', $currency_symbol . ' ' ),
+ );
+ $report = new WC_Admin_Report();
+ $filter = static function () use ( $currency_symbol ): string {
+ return $currency_symbol;
+ };
+
+ add_filter( 'woocommerce_currency_symbol', $filter );
+ try {
+ foreach ( $positions as $position => $expected ) {
+ update_option( 'woocommerce_currency_pos', $position );
+ $fragment = $report->get_currency_tooltip();
+
+ $this->assertSame( 1, preg_match( '/^(append_tooltip|prepend_tooltip): (.+)$/s', $fragment, $matches ) );
+ $this->assertSame( $expected[0], $matches[1] );
+ $this->assertSame( $expected[1], json_decode( $matches[2], true, 512, JSON_THROW_ON_ERROR ) );
+ }
+ } finally {
+ remove_filter( 'woocommerce_currency_symbol', $filter );
+ if ( null === $currency_pos ) {
+ delete_option( 'woocommerce_currency_pos' );
+ } else {
+ update_option( 'woocommerce_currency_pos', $currency_pos );
+ }
+ }
+ }
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/admin/reports/class-wc-tests-report-sales-by-date.php b/plugins/woocommerce/tests/legacy/unit-tests/admin/reports/class-wc-tests-report-sales-by-date.php
index 569eed535c4..204220c7ba7 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/admin/reports/class-wc-tests-report-sales-by-date.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/admin/reports/class-wc-tests-report-sales-by-date.php
@@ -267,4 +267,51 @@ class WC_Tests_Report_Sales_By_Date extends WC_Unit_Test_Case {
return $data;
}
+
+ /**
+ * @testdox The rendered sales chart preserves filtered currency symbols in valid JavaScript strings.
+ */
+ public function test_main_chart_encodes_filtered_currency_symbols(): void {
+ $currency_symbol = "'\"\\</script>€雪";
+ $currency_pos = get_option( 'woocommerce_currency_pos', null );
+ $filter = static function () use ( $currency_symbol ): string {
+ return $currency_symbol;
+ };
+ $report = new WC_Report_Sales_By_Date();
+ $report->calculate_current_range( '7day' );
+ $report->get_report_data();
+ $report->chart_colours = array(
+ 'sales_amount' => '#b1d4ea',
+ 'net_sales_amount' => '#3498db',
+ 'average' => '#b1d4ea',
+ 'net_average' => '#3498db',
+ 'order_count' => '#dbe1e3',
+ 'item_count' => '#ecf0f1',
+ 'shipping_amount' => '#5cc488',
+ 'coupon_amount' => '#f1c40f',
+ 'refund_amount' => '#e74c3c',
+ );
+
+ update_option( 'woocommerce_currency_pos', 'left' );
+ add_filter( 'woocommerce_currency_symbol', $filter );
+ ob_start();
+ try {
+ $report->get_main_chart();
+ $output = (string) ob_get_contents();
+ } finally {
+ ob_end_clean();
+ remove_filter( 'woocommerce_currency_symbol', $filter );
+ if ( null === $currency_pos ) {
+ delete_option( 'woocommerce_currency_pos' );
+ } else {
+ update_option( 'woocommerce_currency_pos', $currency_pos );
+ }
+ }
+
+ $this->assertSame( 1, substr_count( $output, '</script>' ) );
+ $this->assertSame( 5, preg_match_all( '/prepend_tooltip:\s*("(?:\\\\.|[^"\\\\])*")/', $output, $matches ) );
+ foreach ( $matches[1] as $encoded_symbol ) {
+ $this->assertSame( $currency_symbol, json_decode( $encoded_symbol, true, 512, JSON_THROW_ON_ERROR ) );
+ }
+ }
}
diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-settings-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-settings-test.php
index adaa7ce4299..286ee36d365 100644
--- a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-settings-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-settings-test.php
@@ -380,6 +380,53 @@ class WC_Admin_Settings_Test extends WC_Unit_Test_Case {
$this->assertSame( 0, $xpath->query( $description . '//script | ' . $description . '//iframe' )->length );
}
+ /**
+ * @testdox Should keep single-page-select attributes inside their rendered contexts.
+ */
+ public function test_output_fields_escapes_single_select_page_attributes(): void {
+ $css = "width: 12px;' onfocus='alert(1)";
+ $class = "safe-class' data-pwned='1";
+ $this->factory->post->create(
+ array(
+ 'post_type' => 'page',
+ 'post_status' => 'publish',
+ )
+ );
+ $options = array(
+ array(
+ 'id' => 'test_page_setting',
+ 'title' => 'Page setting',
+ 'type' => 'single_select_page',
+ 'value' => 0,
+ 'css' => $css,
+ 'class' => $class,
+ ),
+ );
+
+ ob_start();
+ try {
+ WC_Admin_Settings::output_fields( $options );
+ $output = (string) ob_get_contents();
+ } finally {
+ ob_end_clean();
+ }
+
+ $document = new DOMDocument();
+ $previous_state = libxml_use_internal_errors( true );
+ $loaded = $document->loadHTML( '<table>' . $output . '</table>' );
+ libxml_clear_errors();
+ libxml_use_internal_errors( $previous_state );
+
+ $this->assertTrue( $loaded, 'The setting output should remain valid enough for DOM parsing.' );
+
+ $select = ( new DOMXPath( $document ) )->query( '//select[@id="test_page_setting"]' )->item( 0 );
+ $this->assertInstanceOf( DOMElement::class, $select );
+ $this->assertSame( $css, $select->getAttribute( 'style' ) );
+ $this->assertSame( $class, $select->getAttribute( 'class' ) );
+ $this->assertFalse( $select->hasAttribute( 'onfocus' ) );
+ $this->assertFalse( $select->hasAttribute( 'data-pwned' ) );
+ }
+
/**
* @testdox Should not emit a shared "-title" ID for radio settings that have no ID.
*/