CVE-2022-28367: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: OWASP AntiSamy before 1.6.6 allows XSS via HTML tag smuggling on STYLE content with crafted input. The output serializer does not properly encode the supposed Cascading Style Sheets (CSS) content.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2022-04-21T23:15Z
Repository: https://github.com/nahsra/antisamy
Language: Java
Files changed: src/main/java/org/owasp/validator/html/scan/AntiSamyDOMScanner.java, src/test/java/org/owasp/validator/html/test/AntiSamyTest.java, src/test/java/org/owasp/validator/html/test/TestPolicy.java
Commit date: 2022-03-27 11:04:31 -0300
Commit message: Support multiple children handling on style tags

Diff: diff --git a/src/main/java/org/owasp/validator/html/scan/AntiSamyDOMScanner.java b/src/main/java/org/owasp/validator/html/scan/AntiSamyDOMScanner.java
index eb7ffaa..a2ed739 100644
--- a/src/main/java/org/owasp/validator/html/scan/AntiSamyDOMScanner.java
+++ b/src/main/java/org/owasp/validator/html/scan/AntiSamyDOMScanner.java
@@ -405,14 +405,21 @@ public class AntiSamyDOMScanner extends AbstractAntiSamyScanner {
          * Invoke the css parser on this element.
          */
         CssScanner styleScanner = new CssScanner(policy, messages, policy.isEmbedStyleSheets());
 
         try {
-            Node firstChild = ele.getFirstChild();
-            if (firstChild != null) {
+            if (ele.getChildNodes().getLength() > 0) {
+                String toScan = "";
+
+                for (int i = 0; i < ele.getChildNodes().getLength(); i++) {
+                    Node childNode = ele.getChildNodes().item(i);
+                    if (!toScan.isEmpty()){
+                        toScan += "\n";
+                    }
+                    toScan += childNode.getTextContent();
+                }
 
-                String toScan = firstChild.getNodeValue();
                 CleanResults cr = styleScanner.scanStyleSheet(toScan, policy.getMaxInputSize());
                 errorMessages.addAll(cr.getErrorMessages());
 
                 /*
                  * If IE gets an empty style tag, i.e. <style/> it will
@@ -420,16 +427,21 @@ public class AntiSamyDOMScanner extends AbstractAntiSamyScanner {
                  * if after validation no CSS properties are left, we
                  * would normally be left with an empty style tag and
                  * break all CSS. To prevent that, we have this check.
                  */
 
-                final String cleanHTML = cr.getCleanHTML();
+                String cleanHTML = cr.getCleanHTML();
+                cleanHTML = cleanHTML == null || cleanHTML.equals("") ? "/* */" : cleanHTML;
 
-                if (cleanHTML == null || cleanHTML.equals("")) {
-                    firstChild.setNodeValue("/* */");
-                } else {
-                    firstChild.setNodeValue(cleanHTML);
+                ele.getFirstChild().setNodeValue(cleanHTML);
+                /*
+                 * Remove every other node after cleaning CSS, there will
+                 * be only one node in the end, as it always should have.
+                 */
+                for (int i = 1; i < ele.getChildNodes().getLength(); i++) {
+                    Node childNode = ele.getChildNodes().item(i);
+                    ele.removeChild(childNode);
                 }
             }
 
         } catch (DOMException | ScanException | ParseException | NumberFormatException e) {
 
diff --git a/src/test/java/org/owasp/validator/html/test/AntiSamyTest.java b/src/test/java/org/owasp/validator/html/test/AntiSamyTest.java
index f74c9f2..d1ab20b 100644
--- a/src/test/java/org/owasp/validator/html/test/AntiSamyTest.java
+++ b/src/test/java/org/owasp/validator/html/test/AntiSamyTest.java
@@ -1700,7 +1700,21 @@ static final String test33 = "<html>\n"
 
         result = as.scan(input, policy, AntiSamy.SAX);
         assertThat(result.getErrorMessages().size(), is(1));
         assertThat(result.getCleanHTML(), both(containsString("img")).and(not(containsString("CURSOR"))));
     }
+
+    @Test
+    public void testSmuggledTagsInStyleContent() throws ScanException, PolicyException {
+        // HTML tags may be smuggled into a style tag after parsing input to an internal representation.
+        // If that happens, they should be treated as text content and not as children nodes.
+
+        Policy revised = policy.cloneWithDirective(Policy.USE_XHTML,"true");
+        assertThat(as.scan("<style/>b<![cdata[</style><a href=javascript:alert(1)>test", revised, AntiSamy.DOM).getCleanHTML(), not(containsString("javascript")));
+        assertThat(as.scan("<style/>b<![cdata[</style><a href=javascript:alert(1)>test", revised, AntiSamy.SAX).getCleanHTML(), not(containsString("javascript")));
+
+        Policy revised2 = policy.cloneWithDirective(Policy.USE_XHTML,"false");
+        assertThat(as.scan("<select<style/>W<xmp<script>alert(1)</script>", revised2, AntiSamy.DOM).getCleanHTML(), not(containsString("script")));
+        assertThat(as.scan("<select<style/>W<xmp<script>alert(1)</script>", revised2, AntiSamy.SAX).getCleanHTML(), not(containsString("script")));
+    }
 }
 
diff --git a/src/test/java/org/owasp/validator/html/test/TestPolicy.java b/src/test/java/org/owasp/validator/html/test/TestPolicy.java
index 4b71ebb..435175b 100644
--- a/src/test/java/org/owasp/validator/html/test/TestPolicy.java
+++ b/src/test/java/org/owasp/validator/html/test/TestPolicy.java
@@ -41,11 +41,11 @@ import org.owasp.validator.html.model.Tag;
 /**
  * @author Kristian Rosenvold
  */
 public class TestPolicy extends InternalPolicy {
 
-    protected TestPolicy(Policy.ParseContext parseContext) throws PolicyException {
+    protected TestPolicy(Policy.ParseContext parseContext) {
         super(parseContext);
     }
 
     protected TestPolicy(Policy old, Map<String, String> directives, Map<String, Tag> tagRules, Map<String, Property> cssRules) {
         super(old, directives, tagRules, cssRules);

Vulnerable code:             Node firstChild = ele.getFirstChild();
            if (firstChild != null) {
                String toScan = firstChild.getNodeValue();
                final String cleanHTML = cr.getCleanHTML();
                if (cleanHTML == null || cleanHTML.equals("")) {
                    firstChild.setNodeValue("/* */");
                } else {
                    firstChild.setNodeValue(cleanHTML);
    protected TestPolicy(Policy.ParseContext parseContext) throws PolicyException {
Fixed code:             if (ele.getChildNodes().getLength() > 0) {
                String toScan = "";

                for (int i = 0; i < ele.getChildNodes().getLength(); i++) {
                    Node childNode = ele.getChildNodes().item(i);
                    if (!toScan.isEmpty()){
                        toScan += "\n";
                    }
                    toScan += childNode.getTextContent();
                }
                String cleanHTML = cr.getCleanHTML();
                cleanHTML = cleanHTML == null || cleanHTML.equals("") ? "/* */" : cleanHTML;
                ele.getFirstChild().setNodeValue(cleanHTML);
                /*
                 * Remove every other node after cleaning CSS, there will
                 * be only one node in the end, as it always should have.
                 */
                for (int i = 1; i < ele.getChildNodes().getLength(); i++) {
                    Node childNode = ele.getChildNodes().item(i);
                    ele.removeChild(childNode);

    @Test
    public void testSmuggledTagsInStyleContent() throws ScanException, PolicyException {
        // HTML tags may be smuggled into a style tag after parsing input to an internal representation.
        // If that happens, they should be treated as text content and not as children nodes.

        Policy revised = policy.cloneWithDirective(Policy.USE_XHTML,"true");
        assertThat(as.scan("<style/>b<![cdata[</style><a href=javascript:alert(1)>test", revised, AntiSamy.DOM).getCleanHTML(), not(containsString("javascript")));
        assertThat(as.scan("<style/>b<![cdata[</style><a href=javascript:alert(1)>test", revised, AntiSamy.SAX).getCleanHTML(), not(containsString("javascript")));

        Policy revised2 = policy.cloneWithDirective(Policy.USE_XHTML,"false");
        assertThat(as.scan("<select<style/>W<xmp<script>alert(1)</script>", revised2, AntiSamy.DOM).getCleanHTML(), not(containsString("script")));
        assertThat(as.scan("<select<style/>W<xmp<script>alert(1)</script>", revised2, AntiSamy.SAX).getCleanHTML(), not(containsString("script")));
    }
    protected TestPolicy(Policy.ParseContext parseContext) {