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',