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&hellip;', '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&hellip;', '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: "&nbsp;' . get_woocommerce_currency_symbol() . '"';
+				$currency_tooltip = 'append_tooltip: ' . wp_json_encode( '&nbsp;' . $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() . '&nbsp;"';
+				$currency_tooltip = 'prepend_tooltip: ' . wp_json_encode( $currency_symbol . '&nbsp;', 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', '&nbsp;' . $currency_symbol ),
+			'left'        => array( 'prepend_tooltip', $currency_symbol ),
+			'left_space'  => array( 'prepend_tooltip', $currency_symbol . '&nbsp;' ),
+		);
+		$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.
 	 */