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
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<b
a<b
Yes (format)
a>b
a>b
a>b
Yes
a&b
a&b
a&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!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
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
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
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
a}b
a}b
a}b
Yes
a|b
a|b
a|b
Yes
a\b
a\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
&
& (hex)
& (named)
<
< (hex)
< (named)
'
' (hex)
' (decimal)
"
" (hex)
" (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
Test failures: Unit tests comparing exact encoded output will fail
String length: OWASP output is shorter (more efficient)
Readability: OWASP output is more human-readable
Functionally equivalent: Both decode to the same result in browsers
Recommendation
Update tests to either:
Compare decoded values rather than encoded strings
Accept both encoding formats as valid
Use pattern matching that allows for different entity formats
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)