Can Lucee 7 be configured to use the same XSS encoding as Lucee 6?

I’m trying to test our application under Lucee 7 (7.0.2.51-SNAPSHOT), but running into an issue with encodings which is causing a ton of unit tests to fail, which makes it hard to figure out which tests are actually broken vs ones that are failing because of encoding differences.

For example, in Lucee 6 and below, running the following:

encodeForHtmlAttribute('hello world')

Would produce:

hello world

However, in Lucee 7 you get:

hello world

While technically I prefer the output in v7, because it was encoding a lot of characters that did not need to be encoded, but this is breaking a lot of our tests which are testing for encoding.

While I can certainly refactor all of our failing tests, is there a way to get encoding to match Lucee 6 and earlier? If I could add in an environment switch, it would at least temporarily help reveal parts of our code that are actually problematic.

7.0 is bundled with the new v3 ext (announcement coming, I have a long list ATM) which switches to the lightweight OWSAP encoder

It’s smaller and way less complicated that the ESAPI Library we have been using, which still requires libs with CVEs and is a bit of a clusterfuck and has caused endless problems and tickets

https://luceeserver.atlassian.net/browse/LDEV-5960

WORKAROUND you can simply downgrade to the last v2 version

ESAPI vs OWASP Encoder: encodeForHTMLAttribute Comparison

Background

Lucee 7’s ESAPI extension (v3.0.0.11-BETA) switched from the old ESAPI library to OWASP Encoder on January 7, 2026 (commit b28c5c8). This changes the behaviour of encodeForHTMLAttribute() and related functions.

Summary

Library Approach Example: "hello world"
ESAPI (Lucee 6) Encodes almost everything hello world
OWASP Encoder (Lucee 7) Encodes only dangerous chars hello world

Detailed Character Comparison

Input ESAPI (Lucee 6) OWASP (Lucee 7) Different?
hello world hello world hello world Yes
hello\tworld hello	world hello\tworld Yes
hello\nworld hello
world hello\nworld Yes
a=b a=b a=b Yes
a:b a:b a:b Yes
a/b a/b a/b Yes
a'b a'b a'b Yes (format)
a"b a"b a"b Yes (format)
a<b a&#x3c;b a&lt;b Yes (format)
a>b a&#x3e;b a>b Yes
a&b a&#x26;b a&amp;b Yes (format)
a,b a,b a,b No
a.b a.b a.b No
a-b a-b a-b No
a_b a_b a_b No
a!b a&#x21;b a!b Yes
a@b a&#x40;b a@b Yes
a#b a&#x23;b a#b Yes
a$b a&#x24;b a$b Yes
a%b a&#x25;b a%b Yes
a^b a&#x5e;b a^b Yes
a*b a&#x2a;b a*b Yes
a(b a&#x28;b a(b Yes
a)b a&#x29;b a)b Yes
a+b a&#x2b;b a+b Yes
a;b a&#x3b;b a;b Yes
a`b a&#x60;b a`b Yes
a~b a&#x7e;b a~b Yes
a[b a&#x5b;b a[b Yes
a]b a&#x5d;b a]b Yes
a{b a&#x7b;b a{b Yes
a}b a&#x7d;b a}b Yes
a|b a&#x7c;b a|b Yes
a\b a&#x5c;b a\b Yes

Immune Characters (Not Encoded)

Library Immune Characters
ESAPI Alphanumeric + , . - _
OWASP Everything except & < ' "

Entity Format Differences

Even for characters both libraries encode, the format differs:

Character ESAPI OWASP
& &#x26; (hex) &amp; (named)
< &#x3c; (hex) &lt; (named)
' &#x27; (hex) &#39; (decimal)
" &#x22; (hex) &#34; (decimal)

Security Analysis

Both approaches are equally secure for quoted HTML attributes. The difference is philosophical:

  • ESAPI: “Encode everything that isn’t explicitly safe” (paranoid/allowlist)
  • OWASP: “Encode only what’s actually dangerous” (minimal/blocklist)

In a properly quoted HTML attribute like value="..." or value='...':

  • Spaces, tabs, newlines are safe
  • Equals signs, colons, slashes are safe
  • Most punctuation is safe
  • Only &, <, and the quote character used need encoding

Why This Matters

  1. Test failures: Unit tests comparing exact encoded output will fail
  2. String length: OWASP output is shorter (more efficient)
  3. Readability: OWASP output is more human-readable
  4. Functionally equivalent: Both decode to the same result in browsers

Recommendation

Update tests to either:

  1. Compare decoded values rather than encoded strings
  2. Accept both encoding formats as valid
  3. Use pattern matching that allows for different entity formats

Source Code References

  • ESAPI immune chars: DefaultEncoder.java line 97: IMMUNE_HTMLATTR = {',', '.', '-', '_'}
  • OWASP attribute encoding: XMLEncoder.java Mode.ATTRIBUTE encodes only "&<\'\""
  • Lucee extension switch: extension-esapi commit b28c5c8 (Jan 7, 2026)

I tried downgrading, but now I’m getting the following exception stack trying to start up my application (or even trying to view the extension in the Admin):

lucee.runtime.exp.NativeException: java.lang.StackOverflowError
 at java.base/java.lang.Class.getPackage(Class.java:1128)
 at org.apache.felix.framework.util.SecureAction.getAccessor(SecureAction.java:1144)
 at org.apache.felix.framework.util.SecureAction.setAccesssible(SecureAction.java:1022)
 at org.apache.felix.framework.capabilityset.CapabilitySet.coerceType(CapabilitySet.java:616)
 at org.apache.felix.framework.capabilityset.CapabilitySet.compare(CapabilitySet.java:435)
 at org.apache.felix.framework.capabilityset.CapabilitySet.match(CapabilitySet.java:258)
 at org.apache.felix.framework.capabilityset.CapabilitySet.match(CapabilitySet.java:210)
 at org.apache.felix.framework.capabilityset.CapabilitySet.match(CapabilitySet.java:187)
 at org.apache.felix.framework.StatefulResolver.findProvidersInternal(StatefulResolver.java:286)
 at org.apache.felix.framework.ResolveContextImpl.findProviders(ResolveContextImpl.java:114)
 at org.apache.felix.resolver.Candidates.populate(Candidates.java:208)
 at org.apache.felix.resolver.ResolverImpl.getInitialCandidates(ResolverImpl.java:542)
 at org.apache.felix.resolver.ResolverImpl.doResolve(ResolverImpl.java:431)
 at org.apache.felix.resolver.ResolverImpl.resolve(ResolverImpl.java:420)
 at org.apache.felix.resolver.ResolverImpl.resolve(ResolverImpl.java:374)
 at org.apache.felix.framework.StatefulResolver.resolve(StatefulResolver.java:488)
 at org.apache.felix.framework.Felix.resolveBundleRevision(Felix.java:4393)
 at org.apache.felix.framework.Felix.startBundle(Felix.java:2308)
 at org.apache.felix.framework.BundleImpl.start(BundleImpl.java:1006)
 at org.apache.felix.framework.BundleImpl.start(BundleImpl.java:992)
 at lucee.loader.osgi.BundleUtil.start(BundleUtil.java:112)
 at lucee.runtime.osgi.OSGiUtil._start(OSGiUtil.java:1483)
 at lucee.runtime.osgi.OSGiUtil._startIfNecessary(OSGiUtil.java:1445)
 at lucee.runtime.osgi.OSGiUtil._loadBundle(OSGiUtil.java:748)
 at lucee.runtime.osgi.OSGiUtil.loadBundles(OSGiUtil.java:1563)
 at lucee.runtime.osgi.OSGiUtil._start(OSGiUtil.java:1480)
 at lucee.runtime.osgi.OSGiUtil.start(OSGiUtil.java:1455)
 at lucee.runtime.osgi.OSGiUtil._startIfNecessary(OSGiUtil.java:1445)
 at lucee.runtime.osgi.OSGiUtil._loadBundle(OSGiUtil.java:748)
 at lucee.runtime.osgi.OSGiUtil.loadBundle(OSGiUtil.java:671)
 at lucee.runtime.osgi.OSGiUtil.loadBundlesAndPackagesFromMessage(OSGiUtil.java:2562)
 at lucee.runtime.osgi.OSGiUtil.resolveBundleLoadingIssues(OSGiUtil.java:2441)
 at lucee.runtime.osgi.OSGiUtil._start(OSGiUtil.java:1499)
 at lucee.runtime.osgi.OSGiUtil._startIfNecessary(OSGiUtil.java:1445)
 at lucee.runtime.osgi.OSGiUtil._loadBundle(OSGiUtil.java:748)
 at lucee.runtime.osgi.OSGiUtil.loadBundles(OSGiUtil.java:1563)
 at lucee.runtime.osgi.OSGiUtil._start(OSGiUtil.java:1480)
 at lucee.runtime.osgi.OSGiUtil.start(OSGiUtil.java:1455)
 at lucee.runtime.osgi.OSGiUtil._startIfNecessary(OSGiUtil.java:1445)
 at lucee.runtime.osgi.OSGiUtil._loadBundle(OSGiUtil.java:748)
 at lucee.runtime.osgi.OSGiUtil.loadBundle(OSGiUtil.java:671)
 at lucee.runtime.osgi.OSGiUtil.loadBundlesAndPackagesFromMessage(OSGiUtil.java:2562)
 at lucee.runtime.osgi.OSGiUtil.resolveBundleLoadingIssues(OSGiUtil.java:2441)
 at lucee.runtime.osgi.OSGiUtil._start(OSGiUtil.java:1499)
 at lucee.runtime.osgi.OSGiUtil._startIfNecessary(OSGiUtil.java:1445)
 at lucee.runtime.osgi.OSGiUtil._loadBundle(OSGiUtil.java:748)
 at lucee.runtime.osgi.OSGiUtil.loadBundles(OSGiUtil.java:1563)
 at lucee.runtime.osgi.OSGiUtil._start(OSGiUtil.java:1480)
 at lucee.runtime.osgi.OSGiUtil.start(OSGiUtil.java:1455)
 at lucee.runtime.osgi.OSGiUtil._startIfNecessary(OSGiUtil.java:1445)

It keeps repeating the _loadBundle stack.

Is there a safe way to downgrade?

Does that mean esapiEncode() must be renamed to owaspEncode() :slight_smile:

1 Like

nah, i think esapiEncode(modern=true) is better :slight_smile:

I have created a ticket for this

https://luceeserver.atlassian.net/browse/LDEV-6075