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( "