diff --git a/core/src/main/java/lucee/runtime/exp/MissingIncludeException.java b/core/src/main/java/lucee/runtime/exp/MissingIncludeException.java
index 2481757c3ac..7a2d4cea254 100644
--- a/core/src/main/java/lucee/runtime/exp/MissingIncludeException.java
+++ b/core/src/main/java/lucee/runtime/exp/MissingIncludeException.java
@@ -63,7 +63,9 @@ private void setMapping(PageSource ps) {
}
private void setDetail(PageSource ps) {
- setAdditional(KeyConstants._Detail, "File not found: " + ps.getDisplayPath());
+ // CVE-2026-29519: the requested path is reflected into HTML error output, so escape it here at the
+ // single point where untrusted request-path data enters the exception detail.
+ setAdditional(KeyConstants._Detail, "File not found: " + StringUtil.escapeHTML(StringUtil.emptyIfNull(ps.getDisplayPath())));
}
/**
@@ -74,9 +76,11 @@ public PageSource getPageSource() {
}
private static String createMessage(PageSource pageSource) {
+ // CVE-2026-29519: escape the requested path before it becomes the (HTML-rendered) exception message.
+ String realpath = StringUtil.escapeHTML(StringUtil.emptyIfNull(pageSource.getRealpathWithVirtual()));
String dsp = pageSource.getDisplayPath();
- if (dsp == null) return "Page [" + pageSource.getRealpathWithVirtual() + "] not found";
- return "Page [" + pageSource.getRealpathWithVirtual() + "] [" + dsp + "] not found";
+ if (dsp == null) return "Page [" + realpath + "] not found";
+ return "Page [" + realpath + "] [" + StringUtil.escapeHTML(dsp) + "] not found";
}
@Override
diff --git a/test/tickets/LDEV3027.cfc b/test/tickets/LDEV3027.cfc
new file mode 100644
index 00000000000..c28a25dda9b
--- /dev/null
+++ b/test/tickets/LDEV3027.cfc
@@ -0,0 +1,64 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="security" {
+
+ /*
+ * CVE-2026-29519 / LDEV-3027 — reflected XSS via HTML in the request path.
+ *
+ * When a requested template cannot be found, Lucee raises a
+ * MissingIncludeException whose message and detail embed the requested
+ * path (PageSource.getRealpathWithVirtual() / getDisplayPath()). That
+ * text is reflected into the (HTML) detailed error page unescaped, so an
+ * attacker-supplied path segment such as
+ * /foo/
/index.cfm/
+ * executes as live markup in the victim's browser.
+ *
+ * Fix: escape the requested path where it enters the exception message
+ * and detail (MissingIncludeException), mirroring the existing escaping
+ * of the REST 404 path in PageContextImpl. These tests assert the raw
+ * markup never survives into message/detail and that the escaped form is
+ * present instead.
+ */
+
+ function run( testResults, testBox ) {
+
+ describe( "CVE-2026-29519: MissingInclude must HTML-escape the requested path", function() {
+
+ it( title = "message + detail must not contain raw
markup", body = function( currentSpec ) {
+ var payload = "
";
+ var caught = false;
+ try {
+ // Non-existent template whose path carries the payload — this
+ // is exactly what the request-path vector produces internally.
+ include template = "/#payload#_LDEV3027_does_not_exist.cfm";
+ }
+ catch ( missinginclude e ) {
+ caught = true;
+ // Only a RAW tag-open is dangerous. Escaping turns "<" into "<",
+ // which neutralises the XSS; the attribute text "onerror=" legitimately
+ // survives inside the escaped "<img ... onerror=...>" and must NOT
+ // be asserted against (that was a false-positive in an earlier draft).
+ expect( e.message ).notToInclude( "
" );
+ expect( e.detail ).notToInclude( "