Commit 5af2d32d75f for woocommerce
commit 5af2d32d75f1d6b84c5987c37add50e4d15e15ea
Author: Seghir Nadir <nadir.seghir@gmail.com>
Date: Wed Jul 22 11:23:08 2026 +0200
Sanitize single dismissible store notice before rendering (#66820)
The single-notice branch of StoreNotices rendered the raw notice content
via RawHTML, while every sibling branch renders the sanitizeHTML(
decodeEntities(...) ) copy. Store API error messages carry the product
name and are decoded to live HTML by the notice-creation pipeline, so an
entity-encoded payload in a product name could execute script on the Cart
and Checkout blocks. Render uniqueNotices[0].content (the sanitized copy)
instead, and add a regression test.
diff --git a/plugins/woocommerce/changelog/fix-store-notices-single-notice-xss b/plugins/woocommerce/changelog/fix-store-notices-single-notice-xss
new file mode 100644
index 00000000000..2820f30e3a2
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-store-notices-single-notice-xss
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Sanitize the single dismissible store notice before rendering so entity-encoded HTML in Store API error messages (e.g. a product name) cannot execute script on the Cart and Checkout blocks.
diff --git a/plugins/woocommerce/client/blocks/packages/components/store-notices-container/store-notices.tsx b/plugins/woocommerce/client/blocks/packages/components/store-notices-container/store-notices.tsx
index 55e82661856..f9f5b7641da 100644
--- a/plugins/woocommerce/client/blocks/packages/components/store-notices-container/store-notices.tsx
+++ b/plugins/woocommerce/client/blocks/packages/components/store-notices-container/store-notices.tsx
@@ -144,7 +144,7 @@ const StoreNotices = ( {
key={ 'store-notice-' + status }
{ ...noticeProps }
>
- <RawHTML>{ noticeGroup[ 0 ].content }</RawHTML>
+ <RawHTML>{ uniqueNotices[ 0 ].content }</RawHTML>
</StoreNotice>
) : (
<StoreNotice
diff --git a/plugins/woocommerce/client/blocks/packages/components/store-notices-container/test/index.tsx b/plugins/woocommerce/client/blocks/packages/components/store-notices-container/test/index.tsx
index 71e83998dc1..44c04b1d01a 100644
--- a/plugins/woocommerce/client/blocks/packages/components/store-notices-container/test/index.tsx
+++ b/plugins/woocommerce/client/blocks/packages/components/store-notices-container/test/index.tsx
@@ -180,6 +180,40 @@ describe( 'StoreNoticesContainer', () => {
);
} );
+ it( 'Sanitizes HTML in a single dismissible notice before rendering', async () => {
+ // The notice-creation pipeline (e.g. notify-errors.ts) decodes entities
+ // before storing the message, so live HTML can reach the notice content.
+ // The single-notice render branch must sanitize it via RawHTML rather than
+ // injecting it as-is. See XSS regression.
+ dispatch( noticesStore ).createErrorNotice(
+ 'PWNXSS <img src=x onerror="window.__xss=1">',
+ {
+ id: 'custom-xss-test-error',
+ context: 'test-context',
+ }
+ );
+ const { container } = render(
+ <StoreNoticesContainer context="test-context" />
+ );
+ // The disallowed <img> must be stripped, so no image element is injected.
+ expect( container.querySelector( 'img' ) ).toBeNull();
+ expect( container.innerHTML ).not.toContain( 'onerror' );
+ // The harmless text is still shown (visible + spoken message).
+ expect( screen.getAllByText( /PWNXSS/i ).length ).toBeGreaterThan( 0 );
+ // Clean up notices.
+ await act( () =>
+ dispatch( noticesStore ).removeNotice(
+ 'custom-xss-test-error',
+ 'test-context'
+ )
+ );
+ await waitFor( () => {
+ return (
+ select( noticesStore ).getNotices( 'test-context' ).length === 0
+ );
+ } );
+ } );
+
it( 'Combine same notices from several contexts', async () => {
dispatch( noticesStore ).createErrorNotice( 'Custom generic error', {
id: 'custom-subcontext-test-error',