CVE-2023-0301: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site Scripting (XSS) - Stored in GitHub repository alfio-event/alf.io prior to Alf.io 2.0-M4-2301.
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: 5.4
Published: 2023-01-14T18:15Z
Repository: https://github.com/alfio-event/alf.io
Language: Java
Files changed: src/main/java/alfio/util/MustacheCustomTag.java, src/test/java/alfio/util/MustacheCustomTagTest.java
Commit date: 2022-12-09 10:43:35 +0100
Commit message: restrict protocols (#1150)


Diff: diff --git a/src/main/java/alfio/util/MustacheCustomTag.java b/src/main/java/alfio/util/MustacheCustomTag.java
index fecf3c57..f99d9d97 100644
--- a/src/main/java/alfio/util/MustacheCustomTag.java
+++ b/src/main/java/alfio/util/MustacheCustomTag.java
@@ -166,11 +166,19 @@ public class MustacheCustomTag {
         @Override
         public void setAttributes(Node node, String tagName, Map<String, String> attributes) {
             if (node instanceof Link) {
                 Link l = (Link) node;
                 String destination = StringUtils.trimToEmpty(l.getDestination());
+                var scheme = getScheme(destination);
+                scheme.ifPresent(resolvedScheme -> {
+                    if (!Set.of("http", "https").contains(resolvedScheme)) {
+                        log.info("User tried to set an url with scheme {}, only http/https are accepted, href has been removed", resolvedScheme);
+                        attributes.remove("href");
+                    }
+                });
                 if (UrlUtils.isAbsoluteUrl(destination)) {
+                    // accept only http or https protocols if we have an absolute link, else we override with an empty string
                     attributes.put("target", "_blank");
                     attributes.put("rel", "nofollow noopener noreferrer");
                     var newTabLabel = A11Y_NEW_TAB_LABEL.get();
                     if (newTabLabel != null) {
                         attributes.put("aria-label", ((Text)node.getFirstChild()).getLiteral() + " " + newTabLabel);
@@ -181,10 +189,18 @@ public class MustacheCustomTag {
     }
     public static String renderToHtmlCommonmarkEscaped(String input) {
         return renderToHtmlCommonmarkEscaped(input, null);
     }
 
+    /**
+     * return lowercase scheme if present
+     */
+    private static Optional<String> getScheme(String uri) {
+        var s = StringUtils.trimToEmpty(uri).toLowerCase(Locale.ROOT);
+        return s.indexOf(':') >= 0 ? Optional.of(StringUtils.substringBefore(s, ':')) : Optional.empty();
+    }
+
     public static String renderToHtmlCommonmarkEscaped(String input, String localizedNewWindowLabel) {
         try {
             A11Y_NEW_TAB_LABEL.set(localizedNewWindowLabel);
             Node document = COMMONMARK_PARSER.parse(StringEscapeUtils.escapeHtml4(input));
             return COMMONMARK_RENDERER.render(document);
diff --git a/src/test/java/alfio/util/MustacheCustomTagTest.java b/src/test/java/alfio/util/MustacheCustomTagTest.java
index c2c0c19b..17ef9701 100644
--- a/src/test/java/alfio/util/MustacheCustomTagTest.java
+++ b/src/test/java/alfio/util/MustacheCustomTagTest.java
@@ -85,6 +85,15 @@ public class MustacheCustomTagTest {
         assertEquals("<p>link <a href=\"/test\">bla</a> link</p>\n", MustacheCustomTag.renderToHtmlCommonmarkEscaped("link [bla](/test) link"));
 
         //for absolute link we add target="_blank"
         assertEquals("<p>link <a href=\"http://test\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">bla</a> link</p>\n", MustacheCustomTag.renderToHtmlCommonmarkEscaped("link [bla](http://test) link"));
     }
+
+    @Test
+    public void acceptOnlyHttpOrHttpsProtocols() {
+        assertEquals("<p><a href=\"http://google.com\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">google</a></p>\n", MustacheCustomTag.renderToHtmlCommonmarkEscaped("[google](http://google.com)"));
+        assertEquals("<p><a href=\"https://google.com\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">google</a></p>\n", MustacheCustomTag.renderToHtmlCommonmarkEscaped("[google](https://google.com)"));
+        assertEquals("<p><a>google</a></p>\n", MustacheCustomTag.renderToHtmlCommonmarkEscaped("[google](any:google.com)"));
+        assertEquals("<p><a>google</a></p>\n", MustacheCustomTag.renderToHtmlCommonmarkEscaped("[google](other:google.com)"));
+        assertEquals("<p><a>google</a></p>\n", MustacheCustomTag.renderToHtmlCommonmarkEscaped("[google](protocols:/google.com)"));
+    }
 }
\ No newline at end of file

Fixed code:                 var scheme = getScheme(destination);
                scheme.ifPresent(resolvedScheme -> {
                    if (!Set.of("http", "https").contains(resolvedScheme)) {
                        log.info("User tried to set an url with scheme {}, only http/https are accepted, href has been removed", resolvedScheme);
                        attributes.remove("href");
                    }
                });
                    // accept only http or https protocols if we have an absolute link, else we override with an empty string
    /**
     * return lowercase scheme if present
     */
    private static Optional<String> getScheme(String uri) {
        var s = StringUtils.trimToEmpty(uri).toLowerCase(Locale.ROOT);
        return s.indexOf(':') >= 0 ? Optional.of(StringUtils.substringBefore(s, ':')) : Optional.empty();
    }


    @Test
    public void acceptOnlyHttpOrHttpsProtocols() {
        assertEquals("<p><a href=\"http://google.com\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">google</a></p>\n", MustacheCustomTag.renderToHtmlCommonmarkEscaped("[google](http://google.com)"));
        assertEquals("<p><a href=\"https://google.com\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">google</a></p>\n", MustacheCustomTag.renderToHtmlCommonmarkEscaped("[google](https://google.com)"));
        assertEquals("<p><a>google</a></p>\n", MustacheCustomTag.renderToHtmlCommonmarkEscaped("[google](any:google.com)"));
        assertEquals("<p><a>google</a></p>\n", MustacheCustomTag.renderToHtmlCommonmarkEscaped("[google](other:google.com)"));
        assertEquals("<p><a>google</a></p>\n", MustacheCustomTag.renderToHtmlCommonmarkEscaped("[google](protocols:/google.com)"));
    }