Commit a49c254e491 for woocommerce
commit a49c254e491cfd5ff1c1d17b7f08b58320631fc9
Author: Darren Ethier <darren@roughsmootheng.in>
Date: Mon Aug 31 14:33:22 2026 -0400
Normalize checkbox setting description markup (#68200)
diff --git a/plugins/woocommerce/changelog/fix-checkbox-description-markup b/plugins/woocommerce/changelog/fix-checkbox-description-markup
new file mode 100644
index 00000000000..3b072311bfb
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-checkbox-description-markup
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Normalize checkbox description tooltip markup while preserving supported formatting.
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-settings-api.php b/plugins/woocommerce/includes/abstracts/abstract-wc-settings-api.php
index f1b1b648fd1..738bcb8171a 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-settings-api.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-settings-api.php
@@ -84,7 +84,7 @@ abstract class WC_Settings_API {
* Output the admin options table.
*/
public function admin_options() {
- echo '<table class="form-table">' . $this->generate_settings_html( $this->get_form_fields(), false ) . '</table>'; // WPCS: XSS ok.
+ echo '<table class="form-table">' . $this->generate_settings_html( $this->get_form_fields(), false ) . '</table>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- generate_settings_html() returns built-in or extension-filtered settings field markup.
}
/**
@@ -368,7 +368,7 @@ abstract class WC_Settings_API {
}
if ( $echo ) {
- echo $html; // WPCS: XSS ok.
+ echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $html contains built-in renderer output and extension-filtered custom field markup.
} else {
return $html;
}
@@ -458,7 +458,7 @@ abstract class WC_Settings_API {
?>
<tr valign="top">
<th scope="row" class="titledesc">
- <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
+ <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_tooltip_html() returns sanitized, filterable help-tip markup. ?></label>
</th>
<td class="forminp">
<fieldset>
@@ -515,7 +515,7 @@ abstract class WC_Settings_API {
?>
<tr valign="top">
<th scope="row" class="titledesc">
- <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
+ <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_tooltip_html() returns sanitized, filterable help-tip markup. ?></label>
</th>
<td class="forminp">
<fieldset>
@@ -558,7 +558,7 @@ abstract class WC_Settings_API {
?>
<tr valign="top">
<th scope="row" class="titledesc">
- <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
+ <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_tooltip_html() returns sanitized, filterable help-tip markup. ?></label>
</th>
<td class="forminp">
<fieldset>
@@ -613,7 +613,7 @@ abstract class WC_Settings_API {
?>
<tr valign="top">
<th scope="row" class="titledesc">
- <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
+ <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_tooltip_html() returns sanitized, filterable help-tip markup. ?></label>
</th>
<td class="forminp">
<fieldset>
@@ -658,7 +658,7 @@ abstract class WC_Settings_API {
?>
<tr valign="top">
<th scope="row" class="titledesc">
- <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
+ <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_tooltip_html() returns sanitized, filterable help-tip markup. ?></label>
</th>
<td class="forminp">
<fieldset>
@@ -705,7 +705,7 @@ abstract class WC_Settings_API {
?>
<tr valign="top">
<th scope="row" class="titledesc">
- <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
+ <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_tooltip_html() returns sanitized, filterable help-tip markup. ?></label>
</th>
<td class="forminp">
<fieldset>
@@ -751,7 +751,7 @@ abstract class WC_Settings_API {
?>
<tr valign="top">
<th scope="row" class="titledesc">
- <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
+ <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_tooltip_html() returns sanitized, filterable help-tip markup. ?></label>
</th>
<td class="forminp">
<fieldset>
@@ -809,7 +809,7 @@ abstract class WC_Settings_API {
?>
<tr valign="top">
<th scope="row" class="titledesc">
- <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
+ <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_tooltip_html() returns sanitized, filterable help-tip markup. ?></label>
</th>
<td class="forminp">
<fieldset>
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-shipping-method.php b/plugins/woocommerce/includes/abstracts/abstract-wc-shipping-method.php
index 330ff3fa3b4..733540e10cf 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-shipping-method.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-shipping-method.php
@@ -505,7 +505,7 @@ abstract class WC_Shipping_Method extends WC_Settings_API {
echo '<h2>' . esc_html( $this->get_method_title() ) . '</h2>';
}
echo wp_kses_post( wpautop( $this->get_method_description() ) );
- echo $this->get_admin_options_html(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
+ echo $this->get_admin_options_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Returns the extensible settings renderer's markup.
}
/**
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php
index 1be87edd6d3..5e3245818fd 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php
@@ -386,7 +386,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
?>
<tr class="<?php echo esc_attr( $value['row_class'] ); ?>">
<th scope="row" class="titledesc">
- <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
+ <label for="<?php echo esc_attr( $value['id'] ); ?>"><?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 forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>">
<input
@@ -411,7 +411,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
?>
<tr class="<?php echo esc_attr( $value['row_class'] ); ?>">
<th scope="row" class="titledesc">
- <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
+ <label for="<?php echo esc_attr( $value['id'] ); ?>"><?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 forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>">‎
<span class="colorpickpreview" style="background: <?php echo esc_attr( $option_value ); ?>"> </span>
@@ -440,7 +440,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
?>
<tr class="<?php echo esc_attr( $value['row_class'] ); ?>">
<th scope="row" class="titledesc">
- <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
+ <label for="<?php echo esc_attr( $value['id'] ); ?>"><?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 forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>">
<?php
@@ -474,7 +474,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
?>
<tr class="<?php echo esc_attr( $value['row_class'] ); ?>">
<th scope="row" class="titledesc">
- <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
+ <label for="<?php echo esc_attr( $value['id'] ); ?>"><?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 forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>">
<select
@@ -638,7 +638,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
<?php checked( $option_value, 'yes' ); ?>
<?php echo implode( ' ', $custom_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $custom_attributes values are individually escaped before concatenation. ?>
/> <?php echo $description; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $description is sanitized with wp_kses_post(). ?>
- </label> <?php echo $tooltip_html; // WPCS: XSS ok. ?>
+ </label> <?php echo $tooltip_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- For checkboxes, get_field_description() wraps the tip in a description paragraph sanitized with wp_kses_post(). ?>
<?php
if ( ! isset( $value['checkboxgroup'] ) || 'end' === $value['checkboxgroup'] ) {
@@ -672,7 +672,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
?>
<tr class="<?php echo esc_attr( $value['row_class'] ); ?>">
<th scope="row" class="titledesc">
- <label><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html . $disabled_message; // WPCS: XSS ok. ?></label>
+ <label><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html . $disabled_message; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $tooltip_html is sanitized, filterable markup; $disabled_message is fixed escaped markup. ?></label>
</th>
<td class="forminp image_width_settings">
@@ -706,7 +706,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
?>
<tr class="single_select_page <?php echo esc_attr( $value['row_class'] ); ?>">
<th scope="row" class="titledesc">
- <label><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
+ <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. ?>
@@ -771,7 +771,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
?>
<tr class="<?php echo esc_attr( $value['row_class'] ); ?>">
<th scope="row" class="titledesc">
- <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
+ <label for="<?php echo esc_attr( $value['id'] ); ?>"><?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"><select name="<?php echo esc_attr( $value['field_name'] ); ?>" id="<?php echo esc_attr( $value['id'] ); ?>" style="<?php echo esc_attr( $value['css'] ); ?>" data-placeholder="<?php esc_attr_e( 'Choose a country / region…', 'woocommerce' ); ?>" aria-label="<?php esc_attr_e( 'Country / Region', 'woocommerce' ); ?>" class="wc-enhanced-select">
<?php WC()->countries->country_dropdown_options( $country, $state ); ?>
@@ -795,7 +795,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
?>
<tr class="<?php echo esc_attr( $value['row_class'] ); ?>">
<th scope="row" class="titledesc">
- <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
+ <label for="<?php echo esc_attr( $value['id'] ); ?>"><?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">
<select
@@ -831,7 +831,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
?>
<tr class="<?php echo esc_attr( $value['row_class'] ); ?>">
<th scope="row" class="titledesc">
- <label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
+ <label for="<?php echo esc_attr( $value['id'] ); ?>"><?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">
<input
@@ -910,7 +910,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
}
if ( $tooltip_html && in_array( $value['type'], array( 'checkbox' ), true ) ) {
- $tooltip_html = '<p class="description ' . $error_class . '">' . $tooltip_html . '</p>';
+ $tooltip_html = '<p class="description ' . $error_class . '">' . wp_kses_post( $tooltip_html ) . '</p>';
} elseif ( $tooltip_html ) {
$tooltip_html = wc_help_tip( $tooltip_html );
}
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 bb73ad07185..adaa7ce4299 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
@@ -340,6 +340,46 @@ class WC_Admin_Settings_Test extends WC_Unit_Test_Case {
$this->assertSame( 2, $xpath->query( $radio . '//input[@type="radio"]' )->length );
}
+ /**
+ * @testdox Should preserve supported checkbox description tooltip markup and remove unsupported markup.
+ */
+ public function test_output_fields_normalizes_checkbox_description_tooltip_html(): void {
+ $options = array(
+ array(
+ 'id' => 'test_checkbox_tooltip',
+ 'title' => 'Checkbox title',
+ 'type' => 'checkbox',
+ 'value' => 'no',
+ 'desc_tip' => true,
+ 'desc' => 'Use <strong>supported</strong> <em onclick="unsupported">formatting</em><script>unsupported</script><iframe src="https://example.com">unsupported</iframe>.',
+ ),
+ );
+
+ 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 checkbox setting output should be valid enough for DOM parsing.' );
+
+ $xpath = new DOMXPath( $document );
+ $description = '//td[contains(concat(" ", normalize-space(@class), " "), " forminp-checkbox ")]/fieldset/p[contains(concat(" ", normalize-space(@class), " "), " description ")]';
+
+ $this->assertSame( 1, $xpath->query( $description . '//strong[normalize-space(.)="supported"]' )->length );
+ $this->assertSame( 1, $xpath->query( $description . '//em[normalize-space(.)="formatting"]' )->length );
+ $this->assertSame( 0, $xpath->query( $description . '//*[@onclick]' )->length );
+ $this->assertSame( 0, $xpath->query( $description . '//script | ' . $description . '//iframe' )->length );
+ }
+
/**
* @testdox Should not emit a shared "-title" ID for radio settings that have no ID.
*/