CVE-2023-33962: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: JStachio is a  type-safe Java Mustache templating engine. Prior to version 1.0.1, JStachio fails to escape single quotes `'` in HTML, allowing an attacker to inject malicious code. This vulnerability can be exploited by an attacker to execute arbitrary JavaScript code in the context of other users visiting pages that use this template engine. This can lead to various consequences, including session hijacking, defacement of web pages, theft of sensitive information, or even the propagation of malware.

Version 1.0.1 contains a patch for this issue. To mitigate this vulnerability, the template engine should properly escape special characters, including single quotes. Common practice is to escape `'` as `&#39`. As a workaround, users can avoid this issue by using only double quotes `"` for HTML attributes.
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: 2023-05-30T22:15Z
Repository: https://github.com/jstachio/jstachio
Language: Java
Files changed: api/jstachio/src/main/java/io/jstach/jstachio/escapers/Html.java, api/jstachio/src/main/java/io/jstach/jstachio/escapers/HtmlEscaper.java, test/examples/src/test/java/io/jstach/examples/htmlescaper/HtmlEscapeTest.java, test/examples/src/test/java/io/jstach/examples/htmlescaper/package-info.java, test/examples/src/test/java/io/jstach/examples/lambda/LambdaPartialTest.java
Commit date: 2023-05-28 16:12:24 -0400
Commit message: Fix #157 add more aggressive html5 escaping

Diff: diff --git a/api/jstachio/src/main/java/io/jstach/jstachio/escapers/Html.java b/api/jstachio/src/main/java/io/jstach/jstachio/escapers/Html.java
index 9288efce..d1da3767 100644
--- a/api/jstachio/src/main/java/io/jstach/jstachio/escapers/Html.java
+++ b/api/jstachio/src/main/java/io/jstach/jstachio/escapers/Html.java
@@ -8,17 +8,46 @@ import io.jstach.jstachio.Escaper;
 
 /**
  * Provides a mustache spec based HTML escaper which is the default in normal mustache.
  * <p>
  * The escaper simply escapes:
- * <ul>
- * <li>'&quot;'</li>
- * <li>'&gt;'</li>
- * <li>'&lt;'</li>
- * <li>'&amp;'</li>
- * </ul>
- *
+ * <table border="1">
+ * <caption><strong>Escape table</strong></caption>
+ * <tr>
+ * <th>Character</th>
+ * <th>Escaped String</th>
+ * </tr>
+ * <tr>
+ * <td>'<code>&quot;</code>'</td>
+ * <td>{@value HtmlEscaper#QUOT}</td>
+ * </tr>
+ * <tr>
+ * <td>'<code>&amp;</code>'</td>
+ * <td>{@value HtmlEscaper#AMP}</td>
+ * </tr>
+ * <tr>
+ * <td>'<code>&#x27;</code>'</td>
+ * <td>{@value HtmlEscaper#APOS}</td>
+ * </tr>
+ * <tr>
+ * <td>'<code>&lt;</code>'</td>
+ * <td>{@value HtmlEscaper#LT}</td>
+ * </tr>
+ * <tr>
+ * <td>'<code>=</code>'</td>
+ * <td>{@value HtmlEscaper#EQUAL}</td>
+ * </tr>
+ * <tr>
+ * <td>'<code>&gt;</code>'</td>
+ * <td>{@value HtmlEscaper#GT}</td>
+ * </tr>
+ * <tr>
+ * <td>'<code>&#x60;</code>'</td>
+ * <td>{@value HtmlEscaper#BACK_TICK}</td>
+ * </tr>
+ * </table>
+ * <br />
  * <em>N.B. Unlike many XML escapers this escaper does not differentiate attribute and
  * element content. Furthermore Mustache unlike many other templating languages is content
  * agnostic. If more flexibile attribute escaping is needed a custom lambda could be used
  * to preserve the whitespace in attributes. </em>
  * <p>
diff --git a/api/jstachio/src/main/java/io/jstach/jstachio/escapers/HtmlEscaper.java b/api/jstachio/src/main/java/io/jstach/jstachio/escapers/HtmlEscaper.java
index d426a7f5..4013f1f4 100644
--- a/api/jstachio/src/main/java/io/jstach/jstachio/escapers/HtmlEscaper.java
+++ b/api/jstachio/src/main/java/io/jstach/jstachio/escapers/HtmlEscaper.java
@@ -5,17 +5,23 @@ import io.jstach.jstachio.Output;
 
 enum HtmlEscaper implements Escaper {
 
 	Html;
 
-	private static final String QUOT = "&quot;";
+	static final String QUOT = "&quot;";
 
-	private static final String GT = "&gt;";
+	static final String AMP = "&amp;";
 
-	private static final String LT = "&lt;";
+	static final String APOS = "&#x27;";
 
-	private static final String AMP = "&amp;";
+	static final String LT = "&lt;";
+
+	static final String EQUAL = "&#x3D;";
+
+	static final String GT = "&gt;";
+
+	static final String BACK_TICK = "&#x60;";
 
 	@Override
 	public <A extends Output<E>, E extends Exception> void append(A a, CharSequence s) throws E {
 		s = s == null ? "null" : s;
 		append(a, s, 0, s.length());
@@ -25,53 +31,75 @@ enum HtmlEscaper implements Escaper {
 	public <A extends Output<E>, E extends Exception> void append(A a, CharSequence csq, int start, int end) throws E {
 		csq = csq == null ? "null" : csq;
 		for (int i = start; i < end; i++) {
 			char c = csq.charAt(i);
 			switch (c) {
-				case '&' -> {
+				case '"' -> { // 34
+					a.append(csq, start, i);
+					start = i + 1;
+					a.append(QUOT);
+				}
+				case '&' -> { // 38
 					a.append(csq, start, i);
 					start = i + 1;
 					a.append(AMP);
 
 				}
-				case '<' -> {
+				case '\'' -> { // 39
+					a.append(csq, start, i);
+					start = i + 1;
+					a.append(APOS);
+				}
+				case '<' -> { // 60
 					a.append(csq, start, i);
 					start = i + 1;
 					a.append(LT);
-
 				}
-				case '>' -> {
+				case '=' -> { // 61
+					a.append(csq, start, i);
+					start = i + 1;
+					a.append(EQUAL);
+				}
+				case '>' -> { // 62
 					a.append(csq, start, i);
 					start = i + 1;
 					a.append(GT);
 				}
-				case '"' -> {
+				case '`' -> { // 96
 					a.append(csq, start, i);
 					start = i + 1;
-					a.append(QUOT);
+					a.append(BACK_TICK);
 				}
-
 			}
 		}
 		a.append(csq, start, end);
 
 	}
 
 	@Override
 	public <A extends Output<E>, E extends Exception> void append(A a, char c) throws E {
 		switch (c) {
+			case '"' -> {
+				a.append(QUOT);
+			}
 			case '&' -> {
 				a.append(AMP);
 			}
+			case '\'' -> {
+				a.append(APOS);
+			}
 			case '<' -> {
 				a.append(LT);
 			}
+			case '=' -> {
+				a.append(EQUAL);
+			}
 			case '>' -> {
 				a.append(GT);
 			}
-			case '"' -> {
-				a.append(QUOT);
+			case '`' -> {
+				a.append(BACK_TICK);
 			}
 			default -> {
 				a.append(c);
 			}
 		}
diff --git a/test/examples/src/test/java/io/jstach/examples/htmlescaper/HtmlEscapeTest.java b/test/examples/src/test/java/io/jstach/examples/htmlescaper/HtmlEscapeTest.java
new file mode 100644
index 00000000..777c1efc
--- /dev/null
+++ b/test/examples/src/test/java/io/jstach/examples/htmlescaper/HtmlEscapeTest.java
@@ -0,0 +1,33 @@
+package io.jstach.examples.htmlescaper;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import io.jstach.jstache.JStache;
+
+public class HtmlEscapeTest {
+
+	@JStache(template = """
+			<div alt="{{attr}}">
+			{{body}}
+			</div>
+			""")
+	public record EscapeModel(String attr, String body) {
+
+	}
+
+	@Test
+	public void testName() throws Exception {
+		var m = new EscapeModel("\"&\'<=>`", "\"&\'<=>`");
+		String actual = EscapeModelRenderer.of().execute(m);
+		String expected = """
+				<div alt="&quot;&amp;&#x27;&lt;&#x3D;&gt;&#x60;">
+				&quot;&amp;&#x27;&lt;&#x3D;&gt;&#x60;
+				</div>
+				""";
+
+		assertEquals(expected, actual);
+	}
+
+}
diff --git a/test/examples/src/test/java/io/jstach/examples/htmlescaper/package-info.java b/test/examples/src/test/java/io/jstach/examples/htmlescaper/package-info.java
new file mode 100644
index 00000000..86b4c18c
--- /dev/null
+++ b/test/examples/src/test/java/io/jstach/examples/htmlescaper/package-info.java
@@ -0,0 +1,2 @@
+@org.eclipse.jdt.annotation.NonNullByDefault
+package io.jstach.examples.htmlescaper;
\ No newline at end of file
diff --git a/test/examples/src/test/java/io/jstach/examples/lambda/LambdaPartialTest.java b/test/examples/src/test/java/io/jstach/examples/lambda/LambdaPartialTest.java
index efa44b6f..29c3c180 100644
--- a/test/examples/src/test/java/io/jstach/examples/lambda/LambdaPartialTest.java
+++ b/test/examples/src/test/java/io/jstach/examples/lambda/LambdaPartialTest.java
@@ -24,11 +24,11 @@ public class LambdaPartialTest {
 	public void testParent() throws Exception {
 		LambdaSectionParent m = new LambdaSectionParent("ignore");
 		String actual = JStachio.render(m);
 		String expected = """
 				bingo
-				LambdaSectionParent[stuff=ignore]
+				LambdaSectionParent[stuff&#x3D;ignore]
 								""";
 		assertEquals(expected, actual);
 	}
 
 }

Vulnerable code:  * <ul>
 * <li>'&quot;'</li>
 * <li>'&gt;'</li>
 * <li>'&lt;'</li>
 * <li>'&amp;'</li>
 * </ul>
 *
	private static final String QUOT = "&quot;";
	private static final String GT = "&gt;";
	private static final String LT = "&lt;";
	private static final String AMP = "&amp;";
				case '&' -> {
				case '<' -> {

				case '>' -> {
				case '"' -> {
					a.append(QUOT);

			case '"' -> {
				a.append(QUOT);
				LambdaSectionParent[stuff=ignore]
Fixed code:  * <table border="1">
 * <caption><strong>Escape table</strong></caption>
 * <tr>
 * <th>Character</th>
 * <th>Escaped String</th>
 * </tr>
 * <tr>
 * <td>'<code>&quot;</code>'</td>
 * <td>{@value HtmlEscaper#QUOT}</td>
 * </tr>
 * <tr>
 * <td>'<code>&amp;</code>'</td>
 * <td>{@value HtmlEscaper#AMP}</td>
 * </tr>
 * <tr>
 * <td>'<code>&#x27;</code>'</td>
 * <td>{@value HtmlEscaper#APOS}</td>
 * </tr>
 * <tr>
 * <td>'<code>&lt;</code>'</td>
 * <td>{@value HtmlEscaper#LT}</td>
 * </tr>
 * <tr>
 * <td>'<code>=</code>'</td>
 * <td>{@value HtmlEscaper#EQUAL}</td>
 * </tr>
 * <tr>
 * <td>'<code>&gt;</code>'</td>
 * <td>{@value HtmlEscaper#GT}</td>
 * </tr>
 * <tr>
 * <td>'<code>&#x60;</code>'</td>
 * <td>{@value HtmlEscaper#BACK_TICK}</td>
 * </tr>
 * </table>
 * <br />
	static final String QUOT = "&quot;";
	static final String AMP = "&amp;";
	static final String APOS = "&#x27;";
	static final String LT = "&lt;";

	static final String EQUAL = "&#x3D;";

	static final String GT = "&gt;";

	static final String BACK_TICK = "&#x60;";
				case '"' -> { // 34
					a.append(csq, start, i);
					start = i + 1;
					a.append(QUOT);
				}
				case '&' -> { // 38
				case '\'' -> { // 39
					a.append(csq, start, i);
					start = i + 1;
					a.append(APOS);
				}
				case '<' -> { // 60
				case '=' -> { // 61
					a.append(csq, start, i);
					start = i + 1;
					a.append(EQUAL);
				}
				case '>' -> { // 62
				case '`' -> { // 96
					a.append(BACK_TICK);
			case '"' -> {
				a.append(QUOT);
			}
			case '\'' -> {
				a.append(APOS);
			}
			case '=' -> {
				a.append(EQUAL);
			}
			case '`' -> {
				a.append(BACK_TICK);
package io.jstach.examples.htmlescaper;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

import io.jstach.jstache.JStache;

public class HtmlEscapeTest {

	@JStache(template = """
			<div alt="{{attr}}">
			{{body}}
			</div>
			""")
	public record EscapeModel(String attr, String body) {

	}

	@Test
	public void testName() throws Exception {
		var m = new EscapeModel("\"&\'<=>`", "\"&\'<=>`");
		String actual = EscapeModelRenderer.of().execute(m);
		String expected = """
				<div alt="&quot;&amp;&#x27;&lt;&#x3D;&gt;&#x60;">
				&quot;&amp;&#x27;&lt;&#x3D;&gt;&#x60;
				</div>
				""";

		assertEquals(expected, actual);
	}

}
@org.eclipse.jdt.annotation.NonNullByDefault
package io.jstach.examples.htmlescaper;
				LambdaSectionParent[stuff&#x3D;ignore]