@@ -454,6 +454,158 @@ public static final void testStylingCornerCase() {
454454 assertEquals (want , sanitize (input ));
455455 }
456456
457+ /**
458+ * These 5 tests cover regression scenarios for CVE-2025-66021, which relates to
459+ * improper sanitization of HTML content involving <style> and <noscript> tags.
460+ * The tests ensure that HTMLSanitizer:
461+ * - properly closes any opened elements,
462+ * - only allows allowed elements inside <style> blocks,
463+ * - prevents injection of forbidden HTML or scripts within style or noscript,
464+ * - does not allow unexpected element escape or context breaking.
465+ */
466+
467+ /**
468+ * Test #1:
469+ * Verify that unallowed elements (<div>) injected inside <style> are removed,
470+ * and only allowed content (CSS and allowed elements) remain.
471+ */
472+ @ Test
473+ public static final void testCVE202566021_1 () {
474+ // Arrange: Attempt to inject a <div> inside <style>. Only 'style' and 'noscript' are allowed.
475+ String actualPayload = "<noscript><style>/* user content */.x { font-size: 12px; }<div id=\" evil\" >XSS?</div></style></noscript>" ;
476+ String expectedPayload = "<noscript><style>/* user content */.x { font-size: 12px; }</style></noscript>" ;
477+
478+ HtmlPolicyBuilder htmlPolicyBuilder = new HtmlPolicyBuilder ();
479+ PolicyFactory policy = htmlPolicyBuilder
480+ .allowElements ("style" , "noscript" )
481+ .allowTextIn ("style" )
482+ .toFactory ();
483+
484+ // Act
485+ String sanitized = policy .sanitize (actualPayload );
486+
487+ // Assert
488+ assertEquals (expectedPayload , sanitized );
489+ }
490+
491+ /**
492+ * Test #2:
493+ * Ensure that <script> tags (attempting script injection) are stripped out
494+ * even when they appear inside allowed <style> tags.
495+ */
496+ @ Test
497+ public static final void testCVE202566021_2 () {
498+ // Arrange: Attempt to inject a <script> inside <style>. Only 'style' and 'noscript' are allowed.
499+ String actualPayload = "<noscript><style>/* user content */.x { font-size: 12px; }<script>alert('XSS Attack!')</script></style></noscript>" ;
500+ String expectedPayload = "<noscript><style>/* user content */.x { font-size: 12px; }</style></noscript>" ;
501+
502+ HtmlPolicyBuilder htmlPolicyBuilder = new HtmlPolicyBuilder ();
503+ PolicyFactory policy = htmlPolicyBuilder
504+ .allowElements ("style" , "noscript" )
505+ .allowTextIn ("style" )
506+ .toFactory ();
507+
508+ // Act
509+ String sanitized = policy .sanitize (actualPayload );
510+
511+ // Assert
512+ assertEquals (expectedPayload , sanitized );
513+ }
514+
515+ /**
516+ * Test #3:
517+ * Ensure that, if <div> is allowed, then <div> injected inside <style>
518+ * is retained by the sanitizer (since it is now in the policy).
519+ */
520+ @ Test
521+ public static final void testCVE202566021_3 () {
522+ // Arrange: <div> is now allowed, so it should survive sanitization inside <style>.
523+ String actualPayload = "<noscript><style>/* user content */.x { font-size: 12px; }<div id=\" good\" >ALLOWED?</div></style></noscript>" ;
524+ String expectedPayload = "<noscript><style>/* user content */.x { font-size: 12px; }<div id=\" good\" >ALLOWED?</div></style></noscript>" ;
525+
526+ HtmlPolicyBuilder htmlPolicyBuilder = new HtmlPolicyBuilder ();
527+ PolicyFactory policy = htmlPolicyBuilder
528+ .allowElements ("style" , "noscript" , "div" )
529+ .allowTextIn ("style" )
530+ .toFactory ();
531+
532+ // Act
533+ String sanitized = policy .sanitize (actualPayload );
534+
535+ // Assert
536+ assertEquals (expectedPayload , sanitized );
537+ }
538+
539+ /**
540+ * Test #4:
541+ * Confirm that an attempt to prematurely close <style> with </noscript>, then inject a script,
542+ * does not allow the injected script. Sanitizer closes elements properly and only emits allowed tags.
543+ */
544+ @ Test
545+ public static final void testCVE202566021_4 () {
546+ // Arrange: Try to break out of <style> and <noscript>, then add a script. Only style/noscript/p allowed.
547+ String actualPayload = "<noscript><style></noscript><script>alert(1)</script>" ;
548+ String expectedPayload = "<noscript><style></noscript></style></noscript>" ;
549+
550+ HtmlPolicyBuilder htmlPolicyBuilder = new HtmlPolicyBuilder ();
551+ PolicyFactory policy = htmlPolicyBuilder
552+ .allowElements ("style" , "noscript" , "p" )
553+ .allowTextIn ("style" )
554+ .toFactory ();
555+
556+ // Act
557+ String sanitized = policy .sanitize (actualPayload );
558+
559+ // Assert
560+ assertEquals (expectedPayload , sanitized );
561+ }
562+
563+ /**
564+ * Test #5:
565+ * Like Test #4, but with <p> instead of <noscript>. Ensures sanitizer emits correctly closed tags
566+ * and strips the injected script tag completely.
567+ */
568+ @ Test
569+ public static final void testCVE202566021_5 () {
570+ // Arrange: Try to break out of <style> through <p>, then add a script. Only style/noscript/p allowed.
571+ String actualPayload = "<p><style></p><script>alert(1)</script>" ;
572+ String expectedPayload = "<p><style></p></style></p>" ;
573+
574+ HtmlPolicyBuilder htmlPolicyBuilder = new HtmlPolicyBuilder ();
575+ PolicyFactory policy = htmlPolicyBuilder
576+ .allowElements ("style" , "noscript" , "p" )
577+ .allowTextIn ("style" )
578+ .toFactory ();
579+
580+ // Act
581+ String sanitized = policy .sanitize (actualPayload );
582+
583+ // Assert
584+ assertEquals (expectedPayload , sanitized );
585+ }
586+
587+ /**
588+ * Test that <script> tags with space < script> are sanitized correctly.
589+ */
590+ @ Test
591+ public static final void testCVE202566021_6 () {
592+ // Arrange: Attempt to inject a <script> inside <style>. Only 'style' and 'noscript' elements are allowed.
593+ String actualPayload = "<noscript><style>/* user content */.x { font-size: 12px; }< script>alert('XSS Attack!')</script></style></noscript>" ;
594+ String expectedPayload = "<noscript><style>/* user content */.x { font-size: 12px; }</style></noscript>" ;
595+
596+ HtmlPolicyBuilder htmlPolicyBuilder = new HtmlPolicyBuilder ();
597+ PolicyFactory policy = htmlPolicyBuilder
598+ .allowElements ("style" , "noscript" )
599+ .allowTextIn ("style" )
600+ .toFactory ();
601+
602+ // Act
603+ String sanitized = policy .sanitize (actualPayload );
604+
605+ // Assert
606+ assertEquals (expectedPayload , sanitized );
607+ }
608+
457609 private static String sanitize (@ Nullable String html ) {
458610 StringBuilder sb = new StringBuilder ();
459611 HtmlStreamRenderer renderer = HtmlStreamRenderer .create (
0 commit comments