From 990a0dcd2beb4462c8946bacb46f374877b8f92e Mon Sep 17 00:00:00 2001
From: Jim Manico
Date: Sun, 27 Sep 2026 14:46:48 -0700
Subject: [PATCH 1/7] Fix 1.5.0 release review findings
Harden parser-boundary encoding and EncodedWriter behavior, update compatibility and dependency metadata, and add release regression coverage.
---
.github/workflows/build.yaml | 3 +
CHANGELOG.md | 4 +
compatibility/README.md | 12 +-
compatibility/consumers.py | 96 +++-
compatibility/dependencies/esapi.xml | 39 +-
compatibility/tests/test_guards.py | 24 +
.../java/org/owasp/encoder/CDATAEncoder.java | 156 ++-----
.../main/java/org/owasp/encoder/Encode.java | 119 ++---
.../java/org/owasp/encoder/EncodedWriter.java | 36 +-
.../main/java/org/owasp/encoder/Encoder.java | 11 +-
.../org/owasp/encoder/JavaScriptEncoder.java | 38 +-
.../org/owasp/encoder/XMLCommentEncoder.java | 49 +-
.../org/owasp/encoder/CDATAEncoderTest.java | 66 ++-
.../ContextBoundaryCompositionTest.java | 434 ++++++++++++++++++
.../org/owasp/encoder/EncodedWriterTest.java | 283 ++++++++++++
.../owasp/encoder/JavaScriptEncoderTest.java | 52 ++-
.../owasp/encoder/XMLCommentEncoderTest.java | 10 +-
docs/compatibility-decisions.md | 18 +
docs/contexts.md | 21 +
docs/dependencies.md | 13 +-
docs/usage.md | 35 ++
esapi/README.md | 84 ++--
esapi/pom.xml | 43 ++
.../owasp/encoder/esapi/ESAPIContextTest.java | 21 +-
jakarta/pom.xml | 7 +
.../META-INF/java-encoder-advanced.tld | 54 ++-
.../main/resources/META-INF/java-encoder.tld | 28 +-
.../owasp/encoder/tag/TagEncodingTest.java | 27 +-
jsp/pom.xml | 7 +
.../META-INF/java-encoder-advanced.tld | 54 ++-
.../main/resources/META-INF/java-encoder.tld | 28 +-
.../owasp/encoder/tag/TagEncodingTest.java | 27 +-
pom.xml | 11 +-
scripts/check-effective-pom-scm.py | 116 +++++
scripts/tests/test_release_baselines.py | 76 +++
scripts/tests/test_scm_metadata.py | 84 ++++
36 files changed, 1795 insertions(+), 391 deletions(-)
create mode 100644 core/src/test/java/org/owasp/encoder/ContextBoundaryCompositionTest.java
create mode 100644 scripts/check-effective-pom-scm.py
create mode 100644 scripts/tests/test_release_baselines.py
create mode 100644 scripts/tests/test_scm_metadata.py
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 958b59f..41d9cc7 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -28,6 +28,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
+ fetch-depth: 0
- name: Check versions and isolate Maven storage
run: |
python3 scripts/check-ci-version.py --ref "$GITHUB_REF"
@@ -43,6 +44,8 @@ jobs:
run: python3 -m unittest discover -s scripts/tests
- name: Verify clean reactor including the required Docker/browser test
run: ./mvnw -B -ntp clean verify -PtestJakarta 2>&1 | tee build.log
+ - name: Check published modules' effective SCM metadata
+ run: python3 scripts/check-effective-pom-scm.py
- name: Confirm the Jakarta application contains this reactor's exact JAR
run: |
python3 - <<'PY'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9de16d..cdcbeae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,10 @@ Development builds use `1.5.0-SNAPSHOT`; this is not a published release.
* fix: the ESAPI adapter's `encodeForURL` now encodes individual URL components with UTF-8, including reserved delimiters and literal `+`, instead of preserving whole-URI delimiters. Spaces remain `%20`, null remains the string `"null"`, and unpaired surrogates remain `-`. See the [migration guide](esapi/README.md#url-encoding-migration-in-15-unreleased) for output changes, form-encoding differences, and the retained quoted HTML/CSS/JavaScript contracts [#100](https://github.com/OWASP/owasp-java-encoder/issues/100).
* feat: all four `forJavaScript*` methods encode dollar sign (`$`) as `\x24`, backtick as `\x60`, and opening brace (`{`) as `\x7b` [#129](https://github.com/OWASP/owasp-java-encoder/issues/129). Escaping `{` prevents input after a trusted `$` from completing `${...}`. Encoded output now supports literal text in ordinary (untagged) template literals as well as single- and double-quoted strings. This changes the encoded output while preserving its decoded JavaScript string value. Tagged templates (including `String.raw`), `${...}` expression bodies, JSON, and script URLs are unsupported; each method's HTML context restrictions still apply.
* fix: all four `forJavaScript*` methods escape unpaired UTF-16 surrogates as `\uXXXX`, preserving their JavaScript string values through UTF-8 serialization [#135](https://github.com/OWASP/owasp-java-encoder/issues/135), and escape DEL/C1 controls (U+007F to U+009F) as `\xNN` [#163](https://github.com/OWASP/owasp-java-encoder/issues/163). Valid surrogate pairs and other non-ASCII text remain unescaped except U+2028/U+2029. These are output-fidelity changes; NEL was already ordinary JavaScript string data.
+* fix: the HTML/block JavaScript encoders escape every ASCII character that can contribute to a case-insensitive `` script tokens, including the HTML end-tag delimiters, so any nonempty encoded substring cannot complete a delimiter supplied partly by adjacent trusted literal text. `forCDATA` represents every `]` and `>` with close/reopen sequences, preserving parsed text while breaking every nonempty encoded substring of `]]>`; its String facade grows with actual output instead of eagerly reserving the 13× maximum. `forXmlComment` replaces every hyphen with `~`. These are substantial compatibility-visible output changes; see the [migration record](docs/compatibility-decisions.md#15-parser-boundary-output-migration).
+* fix: `EncodedWriter` now enforces Writer lifecycle semantics: write, append and flush operations fail after close; repeated close is harmless; the first close finalizes pending input and still attempts the delegate close, preserving simultaneous failures with suppressed exceptions. Array-slice writes now use overflow-safe bounds validation, including `Integer.MAX_VALUE`-shaped ranges.
+* build: compare all published artifacts against the immutable 1.4.1 public-API baseline, and verify that every publishable effective POM inherits repository-root SCM connection, developer connection and URL values without module-name suffixes.
+* dependencies: `encoder-esapi` overrides Apache HttpClient to 5.6.4 and HttpCore/Core H2 to 5.4.4. ESAPI's transitive Commons Configuration 1.10 and Commons Lang 2.6 remain in the graph and require an explicit maintainer release disposition; the HTTP overrides do not constitute acceptance of those residual findings.
* feat: add `Encode.forJson` String/Writer methods, the `json` encoder context, and `forJson` tags and EL functions in both JSP and Jakarta tag libraries [#145](https://github.com/OWASP/owasp-java-encoder/issues/145). The caller supplies double quotes. Output uses RFC 8259 string escapes and also escapes HTML script delimiters. Java `null` becomes the text `null` (the JSON string `"null"` when quoted); unpaired surrogates use Unicode escapes and may not interoperate with every JSON consumer. Prefer a serializer for complete JSON documents. The ESAPI adapter retains its existing JSON delegation and null behavior.
* feat: add `forXml11`, `forXml11Content` and `forXml11Attribute` tags and EL functions to the advanced JSP and Jakarta taglibs, and `forXml11` to the basic taglibs [#131](https://github.com/OWASP/owasp-java-encoder/issues/131).
* deprecation: `Encoders.URI` and both `ForUriTag` classes are now deprecated like `Encode.forUri`, whose Javadoc now says what to use instead; the `forUri` TLD descriptions warn about double encoding, the adapter builds show deprecation call sites, and the README has a [forUri migration section](README.md#migrating-from-foruri) [#130](https://github.com/OWASP/owasp-java-encoder/issues/130).
diff --git a/compatibility/README.md b/compatibility/README.md
index 86f820b..fa0319b 100644
--- a/compatibility/README.md
+++ b/compatibility/README.md
@@ -11,7 +11,7 @@ and OSGi consumers. Java 9+ legs additionally run explicit and automatic modules
| `encoder` | Java 8 | No runtime dependencies |
| `encoder-jsp` | Java 8 | JSP 2.2.1, Servlet 3.0.1, EL 2.2.5 (`javax`) |
| `encoder-jakarta-jsp` | Java 8 with compatible container APIs | JSP 3.0.0, Servlet 5.0.0, EL 4.0.0 (`jakarta`) |
-| `encoder-esapi` | Java 8 with compatible ESAPI dependencies | ESAPI 2.7.0.0 and its runtime dependencies |
+| `encoder-esapi` | Java 8 with compatible ESAPI dependencies | ESAPI 2.7.0.0, HttpClient 5.6.4, HttpCore/Core H2 5.4.4, and remaining runtime dependencies |
The Jakarta fixture deliberately uses Servlet 5.0, whose minimum Java SE version
is 8 ([specification](https://jakarta.ee/specifications/servlet/5.0/)). Newer servlet
@@ -84,11 +84,11 @@ fail these guards.
During ordinary `./mvnw verify`, Animal Sniffer checks each library against the Java 8
API signature. This catches linkage such as Java 9's covariant `CharBuffer.flip()`
even when bytecode still has class version 52. japicmp checks public/protected API
-binary and source compatibility against **1.4.0**, the latest available Central
-release when this guard was added; update the pinned baseline only after a newer
-release is available there. Missing types are not broadly ignored: dependencies
-are resolved so inherited API changes remain visible. Both checks run in existing
-`build.yaml` jobs because those jobs reach the `verify` phase.
+binary and source compatibility against **1.4.1**, the immutable release immediately
+preceding 1.5.0. Update the pinned baseline for the next development version. Missing
+types are not broadly ignored: dependencies are resolved so inherited API changes
+remain visible. Both checks run in existing `build.yaml` jobs because those jobs
+reach the `verify` phase.
There are no API exclusions today. A future intentional tag-package move requires
an explicit compatibility decision. If approved, add narrowly scoped japicmp
diff --git a/compatibility/consumers.py b/compatibility/consumers.py
index 3b1756e..458a973 100644
--- a/compatibility/consumers.py
+++ b/compatibility/consumers.py
@@ -15,6 +15,7 @@
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / 'compatibility' / 'src'
+POM_NS = {'p': 'http://maven.apache.org/POM/4.0.0'}
ARTIFACTS = {
'core': ('encoder', 'owasp.encoder', 'org.owasp.encoder', 'org.owasp.encoder', 'CoreConsumer'),
'jsp': ('encoder-jsp', 'owasp.encoder.jsp', 'org.owasp.encoder.jsp', 'org.owasp.encoder.tag', 'TagConsumer'),
@@ -41,6 +42,12 @@
'org.owasp.esapi.errors': None, 'org.owasp.esapi.reference': None},
}
+ESAPI_HTTP_MINIMUMS = {
+ ('org.apache.httpcomponents.client5', 'httpclient5'): (5, 6, 3),
+ ('org.apache.httpcomponents.core5', 'httpcore5'): (5, 4, 3),
+ ('org.apache.httpcomponents.core5', 'httpcore5-h2'): (5, 4, 3),
+}
+
def run(*args, **kwargs):
print('+', ' '.join(map(str, args)), flush=True)
@@ -60,6 +67,38 @@ def clauses(value):
return re.split(r',(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)', value) if value else []
+def dependency_versions(pom, path):
+ """Read literal versions for the HTTP dependencies at a POM path."""
+ versions = {}
+ for dependency in pom.findall(path, POM_NS):
+ coordinate = (dependency.findtext('p:groupId', namespaces=POM_NS),
+ dependency.findtext('p:artifactId', namespaces=POM_NS))
+ if coordinate in ESAPI_HTTP_MINIMUMS:
+ version = dependency.findtext('p:version', namespaces=POM_NS)
+ assert version and '${' not in version, (coordinate, version)
+ versions[coordinate] = version
+ assert set(versions) == set(ESAPI_HTTP_MINIMUMS), versions
+ return versions
+
+
+def validate_esapi_http_versions(published, fixture=None):
+ """Keep consumer evidence aligned with the published, patched graph."""
+ if fixture is not None:
+ assert fixture == published, ('ESAPI consumer fixture differs from published POM',
+ fixture, published)
+ for coordinate, minimum in ESAPI_HTTP_MINIMUMS.items():
+ version = published[coordinate]
+ assert re.match(r'^\d+(?:\.\d+)*$', version), (coordinate, version)
+ actual = tuple(int(part) for part in version.split('.'))
+ assert actual[0] == minimum[0] and actual >= minimum, (
+ coordinate, version, 'minimum supported', '.'.join(map(str, minimum)))
+
+
+def esapi_http_jars(versions):
+ return {coordinate[1] + '-' + version + '.jar'
+ for coordinate, version in versions.items()}
+
+
def manifest(jar):
with zipfile.ZipFile(jar) as archive:
text = archive.read('META-INF/MANIFEST.MF').decode().replace('\r\n', '\n').replace('\n ', '')
@@ -87,6 +126,7 @@ def metadata(kind, jar, core):
actual_ranges[name] = versions[0] if versions else None
assert actual_ranges == IMPORT_RANGES[kind], (kind, imports)
assert not any(x.startswith('java.') for x in imports), imports
+ http_versions = None
with zipfile.ZipFile(jar) as archive, zipfile.ZipFile(core) as core_archive:
names = archive.namelist()
assert len(names) == len(set(names)), (jar, 'duplicate ZIP entries')
@@ -123,28 +163,46 @@ def metadata(kind, jar, core):
resource = name.replace('.', '/') + '.class'
assert resource in names or resource in core_archive.namelist(), (tld, name)
pom = ET.fromstring(archive.read('META-INF/maven/org.owasp.encoder/' + artifact + '/pom.xml'))
- ns = {'p': 'http://maven.apache.org/POM/4.0.0'}
- version = pom.findtext('p:parent/p:version', namespaces=ns)
- assert pom.findtext('p:artifactId', namespaces=ns) == artifact, artifact
+ version = pom.findtext('p:parent/p:version', namespaces=POM_NS)
+ assert pom.findtext('p:artifactId', namespaces=POM_NS) == artifact, artifact
assert jar.name == artifact + '-' + version + '.jar', jar
assert attrs['Bundle-Version'] == version.replace('-SNAPSHOT', '.SNAPSHOT'), attrs
runtime_dependencies = set()
provided_dependencies = set()
- for dep in pom.findall('p:dependencies/p:dependency', ns):
- group = dep.findtext('p:groupId', namespaces=ns)
- scope = dep.findtext('p:scope', default='compile', namespaces=ns)
- coordinate = (group, dep.findtext('p:artifactId', namespaces=ns))
+ direct_http_versions = {}
+ for dep in pom.findall('p:dependencies/p:dependency', POM_NS):
+ group = dep.findtext('p:groupId', namespaces=POM_NS)
+ scope = dep.findtext('p:scope', default='compile', namespaces=POM_NS)
+ coordinate = (group, dep.findtext('p:artifactId', namespaces=POM_NS))
+ if coordinate in ESAPI_HTTP_MINIMUMS:
+ direct_http_versions[coordinate] = dep.findtext(
+ 'p:version', namespaces=POM_NS)
if scope in ('compile', 'runtime'):
runtime_dependencies.add(coordinate)
- assert dep.findtext('p:optional', default='false', namespaces=ns) == 'false', coordinate
+ assert dep.findtext('p:optional', default='false', namespaces=POM_NS) == 'false', coordinate
if scope == 'provided': provided_dependencies.add(coordinate)
expected_dependencies = set() if kind == 'core' else {('org.owasp.encoder', 'encoder')}
- if kind == 'esapi': expected_dependencies.add(('org.owasp.esapi', 'esapi'))
+ if kind == 'esapi':
+ expected_dependencies.update({
+ ('org.owasp.esapi', 'esapi'),
+ ('org.apache.httpcomponents.client5', 'httpclient5'),
+ ('org.apache.httpcomponents.core5', 'httpcore5'),
+ ('org.apache.httpcomponents.core5', 'httpcore5-h2'),
+ })
+ http_versions = dependency_versions(
+ pom, 'p:dependencyManagement/p:dependencies/p:dependency')
+ validate_esapi_http_versions(http_versions)
+ assert set(direct_http_versions) == set(ESAPI_HTTP_MINIMUMS), direct_http_versions
+ for coordinate, direct_version in direct_http_versions.items():
+ assert direct_version is None or direct_version == http_versions[coordinate], (
+ coordinate, 'direct version overrides dependency management', direct_version,
+ http_versions[coordinate])
assert runtime_dependencies == expected_dependencies, (kind, runtime_dependencies)
expected_provided = {'jsp': {('javax.servlet.jsp', 'javax.servlet.jsp-api')},
'jakarta': {('jakarta.servlet.jsp', 'jakarta.servlet.jsp-api')}}.get(kind, set())
assert provided_dependencies == expected_provided, (kind, provided_dependencies)
print('Metadata passed:', jar.name)
+ return http_versions
def source_metadata(kind, source_jar):
@@ -167,10 +225,9 @@ def prepare(args):
raise ValueError('Preparation requires an empty directory; run ./mvnw clean verify or choose a new --directory: ' + str(out))
out.mkdir(parents=True, exist_ok=True)
shutil.copytree(ROOT / 'compatibility/config', out / 'config', dirs_exist_ok=True)
- ns = {'p': 'http://maven.apache.org/POM/4.0.0'}
parent = ET.parse(ROOT / 'pom.xml')
- for dep in parent.findall('p:dependencies/p:dependency', ns):
- assert dep.findtext('p:scope', namespaces=ns) == 'test', ET.tostring(dep)
+ for dep in parent.findall('p:dependencies/p:dependency', POM_NS):
+ assert dep.findtext('p:scope', namespaces=POM_NS) == 'test', ET.tostring(dep)
jars = {}
for kind, (artifact, *_) in ARTIFACTS.items():
candidates = [p for p in (ROOT / kind / 'target').glob(artifact + '-*.jar')
@@ -183,7 +240,16 @@ def prepare(args):
source_metadata(kind, candidates[0].with_name(candidates[0].stem + '-sources.jar'))
with zipfile.ZipFile(candidates[0].with_name(candidates[0].stem + '-javadoc.jar')) as docs:
assert 'index.html' in docs.namelist(), ('missing Javadoc index', kind)
- for kind, jar in jars.items(): metadata(kind, jar, jars['core'])
+ published_http = None
+ for kind, jar in jars.items():
+ result = metadata(kind, jar, jars['core'])
+ if kind == 'esapi':
+ published_http = result
+ fixture_http = dependency_versions(
+ ET.parse(ROOT / 'compatibility/dependencies/esapi.xml').getroot(),
+ 'p:dependencies/p:dependency')
+ validate_esapi_http_versions(published_http, fixture_http)
+ expected_http_jars = esapi_http_jars(published_http)
run('javac', '--release', '9', '-d', out / 'metadata', SOURCE / 'ModuleMetadata.java')
run('java', '-cp', out / 'metadata', 'consumer.ModuleMetadata', *jars.values())
# javac's module discovery does not honor the runtime multi-release property.
@@ -203,6 +269,10 @@ def prepare(args):
'-DincludeScope=runtime', '-DoutputDirectory=' + str(out / 'dependencies' / kind))
for kind, (artifact, explicit, automatic, package, main) in ARTIFACTS.items():
deps = sorted((out / 'dependencies' / kind).glob('*.jar'))
+ if kind == 'esapi':
+ actual_http = {item.name for item in deps
+ if item.name.startswith(('httpclient5-', 'httpcore5-'))}
+ assert actual_http == expected_http_jars, actual_http
artifacts = [jars['core']] + ([jars[kind]] if kind != 'core' else [])
src = out / 'sources' / kind
src.mkdir(parents=True, exist_ok=True)
diff --git a/compatibility/dependencies/esapi.xml b/compatibility/dependencies/esapi.xml
index 4aa3210..851ac23 100644
--- a/compatibility/dependencies/esapi.xml
+++ b/compatibility/dependencies/esapi.xml
@@ -1,3 +1,36 @@
-4.0.0org.owasp.encoder.testsconsumer-esapi1
- org.owasp.esapiesapi2.7.0.0
-
+
+ 4.0.0
+ org.owasp.encoder.tests
+ consumer-esapi
+ 1
+
+
+ org.owasp.esapi
+ esapi
+ 2.7.0.0
+
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ 5.6.4
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.apache.httpcomponents.core5
+ httpcore5
+ 5.4.4
+
+
+ org.apache.httpcomponents.core5
+ httpcore5-h2
+ 5.4.4
+
+
+
diff --git a/compatibility/tests/test_guards.py b/compatibility/tests/test_guards.py
index d6a945e..0532af3 100644
--- a/compatibility/tests/test_guards.py
+++ b/compatibility/tests/test_guards.py
@@ -44,6 +44,30 @@ def test_original_packages_pass(self):
with self.subTest(artifact=kind):
consumers.metadata(kind, jar, self.jars['core'])
+ def test_esapi_consumer_graph_matches_published_pom(self):
+ published = consumers.metadata('esapi', self.jars['esapi'], self.jars['core'])
+ fixture = consumers.dependency_versions(
+ ET.parse(ROOT / 'compatibility/dependencies/esapi.xml').getroot(),
+ 'p:dependencies/p:dependency')
+ consumers.validate_esapi_http_versions(published, fixture)
+
+ stale = dict(fixture)
+ stale[('org.apache.httpcomponents.client5', 'httpclient5')] = '5.6.2'
+ with self.assertRaises(AssertionError):
+ consumers.validate_esapi_http_versions(published, stale)
+
+ def test_esapi_direct_http_version_cannot_override_management(self):
+ def mutate(entries):
+ name = 'META-INF/maven/org.owasp.encoder/encoder-esapi/pom.xml'
+ pom = ET.fromstring(entries[name])
+ ns = '{http://maven.apache.org/POM/4.0.0}'
+ for dependency in pom.findall(ns + 'dependencies/' + ns + 'dependency'):
+ if dependency.findtext(ns + 'artifactId') == 'httpclient5':
+ ET.SubElement(dependency, ns + 'version').text = '5.6.2'
+ break
+ entries[name] = ET.tostring(pom)
+ self.rejected('esapi', mutate)
+
def test_osgi_execution_requirement(self):
def mutate(entries):
name = 'META-INF/MANIFEST.MF'
diff --git a/core/src/main/java/org/owasp/encoder/CDATAEncoder.java b/core/src/main/java/org/owasp/encoder/CDATAEncoder.java
index d08014b..9d85d4c 100644
--- a/core/src/main/java/org/owasp/encoder/CDATAEncoder.java
+++ b/core/src/main/java/org/owasp/encoder/CDATAEncoder.java
@@ -41,47 +41,42 @@
* for including large blocks of text that contain characters that normally
* require encoding (ampersand, quotes, less-than, etc...). The CDATA context
* however still does not allow invalid characters, and can be closed by the
- * sequence "]]>". This encoder removes invalid XML characters, and encodes
- * "]]>" (to "]]]]><![CDATA[>"). The result is that the data integrity is
- * maintained, but the code receiving the output will have to handle multiple
- * CDATA events. As an alternate approach, the caller could pre-encode "]]>" to
- * something of their choosing (e.g. data.replaceAll("\\]\\]>", "]] >")), then
- * use this encoder to remove any invalid XML characters.
+ * sequence "]]>". This encoder removes invalid XML characters and represents
+ * every {@code "]"} and {@code ">"} with close/reopen sequences. That rule
+ * prevents any nonempty encoded substring from completing a {@code "]]>"}
+ * delimiter supplied partly by adjacent trusted literal text. Parsed character
+ * data is preserved, but a streaming XML consumer must handle multiple CDATA
+ * and character-data events.
*
* @author Jeff Ichnowski
*/
class CDATAEncoder extends Encoder {
- /**
- * The encoding of @{code "]]>"}.
- */
- private static final char[] CDATA_END_ENCODED
- = "]]]]>".toCharArray();
+ /** A parsed-value-preserving spelling of {@code "]"}. */
+ private static final char[] RIGHT_BRACKET_ENCODED
+ = "]]>]"}.
- */
- private static final int CDATA_END_ENCODED_LENGTH = 15;
+ /** Length of {@link #RIGHT_BRACKET_ENCODED}. */
+ private static final int RIGHT_BRACKET_ENCODED_LENGTH = 13;
/**
- * Length of {@code "]]>"}.
+ * A value-preserving spelling of {@code ">"} that remains safe when
+ * adjacent trusted CDATA text ends in {@code "]]"}.
*/
- private static final int CDATA_END_LENGTH = 3;
+ private static final char[] GREATER_THAN_ENCODED
+ = "]]>".toCharArray();
+
+ /** Length of {@link #GREATER_THAN_ENCODED}. */
+ private static final int GREATER_THAN_ENCODED_LENGTH = 13;
@Override
protected int maxEncodedLength(int n) {
- // "]" becomes "]" (1 -> 1)
- // "]]" becomes "]]" (2 -> 2)
- // "]]>" becomes "]]]]>" (3 -> 15)
- // "]]>]" becomes "]]]]>]" (3 -> 15 + 1 -> 1)
- // ...
-
- int worstCase = n / CDATA_END_LENGTH;
- int remainder = n % CDATA_END_LENGTH;
-
- return worstCase * CDATA_END_ENCODED_LENGTH + remainder;
-
-// return (n - remainder) * 5 + remainder;
+ // A lone ']' has the largest per-character replacement.
+ if (n > Integer.MAX_VALUE / RIGHT_BRACKET_ENCODED_LENGTH) {
+ // The exact result cannot fit in an int (or a Java String).
+ return Integer.MAX_VALUE;
+ }
+ return n * RIGHT_BRACKET_ENCODED_LENGTH;
}
@Override
@@ -91,38 +86,14 @@ protected int firstEncodedOffset(String input, int off, int len) {
for (int i = off; i < n; ++i) {
char ch = input.charAt(i);
if (ch <= Unicode.MAX_ASCII) {
- if (ch != ']') {
+ if (ch == ']' || ch == '>') {
+ return i;
+ } else {
if (ch < ' ' && ch != '\n' && ch != '\r' && ch != '\t') {
return i;
// } else {
// // valid
}
-
- } else if (i + 1 < n) {
- if (input.charAt(i + 1) != ']') {
- // "]x" (next character is safe for this to be ']')
- } else {
- // "]]?"
- // keep looping through ']'
- for (; i + 2 < n && input.charAt(i + 2) == ']'; ++i) {
- // valid
- }
- // at this point we've looped through a sequence
- // of 2 or more "]", if the next character is ">"
- // we need to encode "]]>".
- if (i + 2 < n) {
- if (input.charAt(i + 2) == '>') {
- return i;
-// } else {
-// // valid
- }
-
- } else {
- return n;
- }
- }
- } else {
- return n;
}
} else if (ch < Character.MIN_HIGH_SURROGATE) {
if (ch <= Unicode.MAX_C1_CTRL_CHAR && ch != Unicode.NEL) {
@@ -174,7 +145,21 @@ protected CoderResult encodeArrays(CharBuffer input, CharBuffer output, boolean
for (; i < n; ++i) {
char ch = in[i];
if (ch <= Unicode.MAX_ASCII) {
- if (ch != ']') {
+ if (ch == ']') {
+ if (m - j < RIGHT_BRACKET_ENCODED_LENGTH) {
+ return overflow(input, i, output, j);
+ }
+ System.arraycopy(RIGHT_BRACKET_ENCODED, 0, out, j,
+ RIGHT_BRACKET_ENCODED_LENGTH);
+ j += RIGHT_BRACKET_ENCODED_LENGTH;
+ } else if (ch == '>') {
+ if (m - j < GREATER_THAN_ENCODED_LENGTH) {
+ return overflow(input, i, output, j);
+ }
+ System.arraycopy(GREATER_THAN_ENCODED, 0, out, j,
+ GREATER_THAN_ENCODED_LENGTH);
+ j += GREATER_THAN_ENCODED_LENGTH;
+ } else {
if (j >= m) {
return overflow(input, i, output, j);
}
@@ -183,61 +168,6 @@ protected CoderResult encodeArrays(CharBuffer input, CharBuffer output, boolean
} else {
out[j++] = XMLEncoder.INVALID_CHARACTER_REPLACEMENT;
}
- } else if (i + 1 < n) {
- if (in[i + 1] != ']') {
- // "]x" (next character is safe for this to be ']')
- if (j >= m) {
- return overflow(input, i, output, j);
- }
- out[j++] = ']';
- } else {
- // "]]?"
- // keep looping through ']'
- for (; i + 2 < n && in[i + 2] == ']'; ++i) {
- if (j >= m) {
- return overflow(input, i, output, j);
- }
- out[j++] = ']';
- }
- // at this point we've looped through a sequence
- // of 2 or more "]", if the next character is ">"
- // we need to encode "]]>".
- if (i + 2 < n) {
- if (in[i + 2] == '>') {
- if (j + CDATA_END_ENCODED_LENGTH > m) {
- return overflow(input, i, output, j);
- }
- System.arraycopy(CDATA_END_ENCODED, 0, out, j, CDATA_END_ENCODED_LENGTH);
- j += CDATA_END_ENCODED_LENGTH;
- i += 2;
- } else {
- if (j >= m) {
- return overflow(input, i, output, j);
- }
- out[j++] = ']';
- }
- } else if (endOfInput) {
- if (j + 2 > m) {
- return overflow(input, i, output, j);
- }
- out[j++] = ']';
- out[j++] = ']';
- i = n;
- break;
- } else {
- break;
- }
- }
- } else if (endOfInput) {
- // seen "]", then end of input.
- if (j >= m) {
- return overflow(input, i, output, j);
- }
- out[j++] = ']';
- i++;
- break;
- } else {
- break;
}
} else if (ch < Character.MIN_HIGH_SURROGATE) {
if (ch > Unicode.MAX_C1_CTRL_CHAR || ch == Unicode.NEL) {
@@ -263,7 +193,7 @@ protected CoderResult encodeArrays(CharBuffer input, CharBuffer output, boolean
out[j++] = XMLEncoder.INVALID_CHARACTER_REPLACEMENT;
++i;
} else {
- if (j + 1 >= m) {
+ if (m - j < 2) {
return overflow(input, i, output, j);
}
out[j++] = ch;
diff --git a/core/src/main/java/org/owasp/encoder/Encode.java b/core/src/main/java/org/owasp/encoder/Encode.java
index 083cbff..f4663d5 100644
--- a/core/src/main/java/org/owasp/encoder/Encode.java
+++ b/core/src/main/java/org/owasp/encoder/Encode.java
@@ -35,6 +35,7 @@
package org.owasp.encoder;
import java.io.IOException;
+import java.io.StringWriter;
import java.io.Writer;
import java.nio.CharBuffer;
import java.nio.charset.CoderResult;
@@ -57,9 +58,9 @@
* coordinating access to a shared output writer.
*
*
For large inputs, prefer the Writer overloads or {@link EncodedWriter}.
- * String overloads may allocate temporary storage for the maximum possible
- * encoded length of the remaining input, even when the actual result is much
- * shorter (up to nine output characters per input character for URI encoding).
+ * String overloads retain the complete encoded result in memory, while the
+ * Writer APIs emit output in fixed-size batches. CDATA can produce up to
+ * thirteen output characters per input character.
*
*
Please make sure to read and understand the context that the method encodes
* for. Encoding for the incorrect context will likely lead to exposing a
@@ -888,8 +889,9 @@ public static void forXmlAttribute(Writer out, String input)
*
*
This method replaces invalid XML 1.0 characters and the additional
* controls and noncharacters described by {@link #forHtml(String)} with spaces,
- * and replaces the "--" sequence (which is invalid in XML comments)
- * with "-~" (hyphen-tilde). This encoding behavior may change
+ * and replaces every hyphen with a tilde. This prevents an input hyphen
+ * from combining with a hyphen in adjacent trusted text to form the forbidden
+ * "--" sequence. This encoding behavior may change
* in future releases. If the comments need to be decoded, the
* caller will need to come up with their own encode/decode system.
*
@@ -1017,9 +1019,13 @@ public static void forXml11Attribute(Writer out, String input)
}
/**
- * Encodes data for an XML CDATA section. On the chance that the input
- * contains a terminating {@code "]]>"}, it will be replaced by
- * {@code "]]]]>"}.
+ * Encodes data for an XML CDATA section. Every {@code "]"} and
+ * {@code ">"} is represented with close/reopen sequences. This preserves
+ * parsed character data and prevents any nonempty encoded substring from
+ * completing a {@code "]]>"} terminator supplied partly by adjacent
+ * trusted literal text.
+ * Each input {@code "]"} or {@code ">"} expands to thirteen output
+ * characters; prefer the Writer overload for large values.
* Invalid XML 1.0 characters and the additional controls and noncharacters
* described by {@link #forHtml(String)} are replaced by a space.
* XML parsers normalize line endings. The caller must provide the CDATA
@@ -1122,6 +1128,16 @@ public static void forJava(Writer out, String input)
*
*
*
+ *
@@ -1271,8 +1303,10 @@ public static void forJavaScript(Writer out, String input)
* NOT safe for use in script blocks. The caller MUST provide the
* surrounding single (') or double (") quotation marks, or backticks (`) for
* an ordinary (untagged) template literal. This method performs the
- * same encode as {@link #forJavaScript(String)} with the
- * exception that / and - are not escaped.
+ * same encode as {@link #forJavaScript(String)} except that slash, hyphen,
+ * space, {@code !}, {@code <}, {@code >}, and both cases of C, I, P, R, S,
+ * and T are not escaped. Those additional escapes protect an outer HTML
+ * script context and do not apply to an event attribute.
*
DEL and C1 controls (U+007F to U+009F) are hex-escaped. Unpaired UTF-16
* surrogates use Unicode escapes; valid surrogate pairs remain unescaped.
*
@@ -1379,10 +1413,10 @@ public static void forJavaScriptBlock(Writer out, String input)
* use in ANY context embedded in HTML. The caller must
* provide surrounding single (') or double (") quotation marks, or backticks
* (`) for an ordinary (untagged) template literal. This method
- * performs the same encode as {@link #forJavaScript(String)} with
- * the exception that /, -, and & are not
- * escaped and " and ' are encoded as
- * \" and \' respectively.
+ * performs the same encode as {@link #forJavaScript(String)} except that
+ * slash, hyphen, space, {@code !}, {@code <}, {@code >}, both cases of C,
+ * I, P, R, S, and T, and ampersand are not escaped; double and single
+ * quotes use backslash escapes.
*
DEL and C1 controls (U+007F to U+009F) are hex-escaped. Unpaired UTF-16
* surrogates use Unicode escapes; valid surrogate pairs remain unescaped.
*
@@ -1638,10 +1672,11 @@ static class Buffer {
/**
* The core String encoding routine of this class. It uses the input
- * and output buffers to allow the encoders to work in reuse arrays.
- * When the input and/or output exceeds the capacity of the reused
- * arrays, temporary ones are allocated and then discarded after
- * the encode is done.
+ * and output buffers to allow the encoders to work in reused arrays.
+ * If the encoded result exceeds the reused output array, the same
+ * fixed-size streaming loop as the Writer facade grows a StringWriter
+ * according to the actual output length. It does not eagerly allocate
+ * the encoder's worst-case expansion.
*
* @param encoder the encoder to use
* @param str the string to encode
@@ -1666,41 +1701,19 @@ String encode(Encoder encoder, String str, int j) {
return new String(_output.array(), 0, _output.position());
}
- // else, it's an overflow, we need to use a new output buffer
- // we'll allocate this buffer to be the exact size of the worst
- // case, guaranteeing a second overflow would not be possible.
- CharBuffer tmp = CharBuffer.allocate(_output.position()
- + encoder.maxEncodedLength(_input.remaining()));
-
- // copy over everything that has been encoded so far
- tmp.put(_output.array(), 0, _output.position());
-
- cr = encoder.encodeArrays(_input, tmp, true);
- if (cr.isOverflow()) {
- throw new AssertionError("unexpected result from encoder");
- }
-
- return new String(tmp.array(), 0, tmp.position());
- } else {
- // the input it too large for our pre-allocated buffers
- // we'll use a temporary direct heap allocation
- final int m = j + encoder.maxEncodedLength(remaining);
- CharBuffer buffer = CharBuffer.allocate(m);
- str.getChars(0, j, buffer.array(), 0);
- str.getChars(j, n, buffer.array(), m - remaining);
-
- CharBuffer input = buffer.duplicate();
- input.limit(m).position(m-remaining);
- buffer.position(j);
-
- CoderResult cr = encoder.encodeArrays(input, buffer, true);
-
- if (cr.isOverflow()) {
- throw new AssertionError("unexpected result from encoder");
- }
+ }
- return new String(buffer.array(), 0, buffer.position());
+ // The input or actual output exceeds the reusable buffers. Grow
+ // only with output that the encoder really emits. In particular,
+ // CDATA's 13x maximum must not force a 13x eager allocation for a
+ // large string containing only one character that needs encoding.
+ StringWriter out = new StringWriter(n);
+ try {
+ encode(encoder, out, str, j);
+ } catch (IOException impossible) {
+ throw new AssertionError("StringWriter threw IOException", impossible);
}
+ return out.toString();
}
/**
diff --git a/core/src/main/java/org/owasp/encoder/EncodedWriter.java b/core/src/main/java/org/owasp/encoder/EncodedWriter.java
index f729a52..7661133 100644
--- a/core/src/main/java/org/owasp/encoder/EncodedWriter.java
+++ b/core/src/main/java/org/owasp/encoder/EncodedWriter.java
@@ -91,6 +91,11 @@ public class EncodedWriter extends Writer {
*/
private CharBuffer _leftOverBuffer;
+ /**
+ * Whether this writer has been closed.
+ */
+ private boolean _closed;
+
/**
* Creates an EncodedWriter that uses the specified encoder to encode all input before sending it to the wrapped writer.
*
@@ -133,6 +138,14 @@ public EncodedWriter(Writer out, String contextName) throws UnsupportedContextEx
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
synchronized (lock) {
+ ensureOpen();
+ if (cbuf == null) {
+ throw new NullPointerException("cbuf must not be null");
+ }
+ if (off < 0 || len < 0 || off > cbuf.length - len) {
+ throw new IndexOutOfBoundsException();
+ }
+
CharBuffer input = CharBuffer.wrap(cbuf);
input.limit(off + len).position(off);
@@ -206,9 +219,21 @@ private void flushLeftOver(CharBuffer input) throws IOException {
_leftOverBuffer.clear();
}
+ /**
+ * Ensures this writer has not been closed.
+ *
+ * @throws IOException if this writer is closed.
+ */
+ private void ensureOpen() throws IOException {
+ if (_closed) {
+ throw new IOException("Writer is closed");
+ }
+ }
+
@Override
public void flush() throws IOException {
synchronized (lock) {
+ ensureOpen();
flushBufferToWriter();
_out.flush();
}
@@ -217,9 +242,14 @@ public void flush() throws IOException {
@Override
public void close() throws IOException {
synchronized (lock) {
- flushLeftOver(null);
- flushBufferToWriter();
- _out.close();
+ if (_closed) {
+ return;
+ }
+ _closed = true;
+ try (Writer ignored = _out) {
+ flushLeftOver(null);
+ flushBufferToWriter();
+ }
}
}
}
diff --git a/core/src/main/java/org/owasp/encoder/Encoder.java b/core/src/main/java/org/owasp/encoder/Encoder.java
index 12eb39d..9aaee5c 100644
--- a/core/src/main/java/org/owasp/encoder/Encoder.java
+++ b/core/src/main/java/org/owasp/encoder/Encoder.java
@@ -108,14 +108,13 @@ public abstract class Encoder {
* UNDERFLOW}, there may be characters left to encode in the
* {@code input} buffer (i.e. {@code input.hasRemaining() ==
* true}). This will happen when the encoder needs to see more
- * input before determining what to do--for example when encoding
- * for CDATA, if the input ends with {@code "foo]]"}, the encoder
- * will need to see the next character to determine if it is a ">"
- * or not.
+ * input before determining what to do--for example when an encoder sees
+ * a high surrogate at the end of a non-final input buffer and needs the
+ * next code unit to determine whether it forms a pair.
*
*
The output buffer must have enough remaining space for an entire
- * encoded sequence. Allow at least 15 characters of output space to support
- * every encoder (CDATA requires 15 for its terminator replacement).
+ * encoded sequence. Allow at least 13 characters of output space to support
+ * every current encoder (CDATA has the longest replacement).
* An undersized buffer can cause repeated {@code OVERFLOW} results without
* advancing either buffer; drain or enlarge it before retrying.
*
diff --git a/core/src/main/java/org/owasp/encoder/JavaScriptEncoder.java b/core/src/main/java/org/owasp/encoder/JavaScriptEncoder.java
index 0cae34f..74fa230 100644
--- a/core/src/main/java/org/owasp/encoder/JavaScriptEncoder.java
+++ b/core/src/main/java/org/owasp/encoder/JavaScriptEncoder.java
@@ -64,9 +64,10 @@ enum Mode {
ATTRIBUTE,
/**
* Encoding for use in HTML script blocks. The main concern here is
- * prematurely terminating a script block with a closing "</" inside
- * the string. This encoding escapes "/" as "\/" to prevent such
- * termination.
+ * prematurely terminating a script block with a closing
+ * "</script" token inside the string. Characters that can contribute
+ * to an HTML script delimiter are escaped so an encoded substring
+ * cannot complete a delimiter supplied partly by trusted literal text.
*/
BLOCK,
/**
@@ -131,16 +132,21 @@ enum Mode {
~((1 << Unicode.DEL) | (1 << '`') | (1 << '{')),};
if (mode == Mode.BLOCK || mode == Mode.HTML) {
- // in " is escaped as "<\/script>" and "". To do so we escape "/" as
+ // "\/" and hex-escape every other ASCII character that could form
+ // part of an HTML script delimiter. This prevents
+ // any non-empty encoded substring from completing "" with adjacent trusted literal text. Other ASCII
+ // whitespace already uses JavaScript escapes in every mode.
+ // A JavaScript backslash escape would not prevent the exploits on
+ // "" and "" to become "--->", which is also invalid. As with all XML-based context, invalid XML characters are not
- * allowed.
+ * XMLCommentEncoder -- Encodes for the XML comment context. The sequence
+ * "--" is not allowed in XML comments. Every input hyphen is replaced so it
+ * cannot combine with a hyphen in adjacent trusted literal text. As with all
+ * XML-based contexts, invalid XML characters are not allowed.
*
* @author Jeff Ichnowski
*/
class XMLCommentEncoder extends Encoder {
/**
- * This is the character used to replace a hyphen when a sequence of hypens is encountered.
+ * This is the character used to replace every input hyphen.
*/
static final char HYPHEN_REPLACEMENT = '~';
@@ -75,15 +75,7 @@ protected int firstEncodedOffset(String input, int off, int len) {
char ch = input.charAt(i);
if (ch <= Unicode.MAX_ASCII) {
if (ch == '-') {
- if (i + 1 < n) {
- if (input.charAt(i + 1) == '-') {
- return i;
-// } else {
-// // valid
- }
- } else {
- return i;
- }
+ return i;
} else if (ch < ' ' && ch != '\n' && ch != '\r' && ch != '\t') {
return i;
// } else {
@@ -130,31 +122,12 @@ protected CoderResult encodeArrays(CharBuffer input, CharBuffer output, boolean
char ch = in[i];
if (ch <= Unicode.MAX_ASCII) {
if (ch == '-') {
- if (i + 1 < n) {
- if (in[i + 1] == '-') {
- if (j + 1 >= m) {
- return overflow(input, i, output, j);
- }
- out[j++] = '-';
- out[j++] = HYPHEN_REPLACEMENT;
- ++i;
- } else {
- if (j >= m) {
- return overflow(input, i, output, j);
- }
- out[j++] = '-';
- }
- } else if (endOfInput) {
- if (j >= m) {
- return overflow(input, i, output, j);
- }
- out[j++] = HYPHEN_REPLACEMENT;
- } else {
- // saw '-' at the end of the buffer, but this is not
- // end of input, we need to see the next character
- // before deciding what to do.
- break;
+ if (j >= m) {
+ return overflow(input, i, output, j);
}
+ // Replacing every hyphen prevents a trusted trailing
+ // hyphen from combining with the start of this input.
+ out[j++] = HYPHEN_REPLACEMENT;
} else if (ch > ' ' || ch == '\n' || ch == '\r' || ch == '\t') {
if (j >= m) {
return overflow(input, i, output, j);
diff --git a/core/src/test/java/org/owasp/encoder/CDATAEncoderTest.java b/core/src/test/java/org/owasp/encoder/CDATAEncoderTest.java
index 510c698..f39ba6a 100644
--- a/core/src/test/java/org/owasp/encoder/CDATAEncoderTest.java
+++ b/core/src/test/java/org/owasp/encoder/CDATAEncoderTest.java
@@ -34,6 +34,8 @@
package org.owasp.encoder;
+import java.io.StringWriter;
+import java.util.Arrays;
import junit.framework.Test;
import junit.framework.TestCase;
@@ -44,20 +46,24 @@
*/
public class CDATAEncoderTest extends TestCase {
public static Test suite() {
+ String rb = "]]>]";
return new EncoderTestSuiteBuilder(CDATAEncoderTest.class, new CDATAEncoder(), "-safe-", "-]]>-")
- .encode("]]]]>", "]]>")
- .encode("]", "]")
- .encode("]]", "]]")
- .encode("]]]]>]", "]]>]")
- .encode("]]]]>]>", "]]>]>")
- .encode("]]]]>>", "]]>>")
- .encode("]]]]]", "]]]]]")
- .encode("<\"&\'>", "<\"&\'>") // valid in CDATA, not in XML
+ .encode(rb + rb + gt, "]]>")
+ .encode(rb, "]")
+ .encode(rb + rb, "]]")
+ .encode(rb + rb + gt + rb, "]]>]")
+ .encode(rb + rb + gt + rb + gt, "]]>]>")
+ .encode(rb + rb + gt + gt, "]]>>")
+ .encode(rb + rb + rb + rb + rb, "]]]]]")
+ .encode(gt, ">")
+ .encode("<\"&'" + gt, "<\"&'>")
.encode("missing-low-surrogate", " x", "\ud800x")
.invalid(0, 0x1f)
.valid("\t\r\n")
.valid(' ', Character.MAX_CODE_POINT)
+ .encoded("]>")
.invalid(0x7f, 0x9f)
.valid("\u0085")
.invalid(Character.MIN_SURROGATE, Character.MAX_SURROGATE)
@@ -89,12 +95,44 @@ public static Test suite() {
public void testMaxEncodedLength() {
CDATAEncoder encoder = new CDATAEncoder();
assertEquals(0, encoder.maxEncodedLength(0));
- assertEquals(1, encoder.maxEncodedLength(1));
- assertEquals(2, encoder.maxEncodedLength(2));
- assertEquals(15, encoder.maxEncodedLength(3));
- assertEquals(16, encoder.maxEncodedLength(4));
- assertEquals(17, encoder.maxEncodedLength(5));
- assertEquals(30, encoder.maxEncodedLength(6));
+ assertEquals(13, encoder.maxEncodedLength(1));
+ assertEquals(26, encoder.maxEncodedLength(2));
+ assertEquals(39, encoder.maxEncodedLength(3));
+ assertEquals(52, encoder.maxEncodedLength(4));
+ assertEquals(65, encoder.maxEncodedLength(5));
+ assertEquals(78, encoder.maxEncodedLength(6));
+ assertEquals(Integer.MAX_VALUE,
+ encoder.maxEncodedLength(Integer.MAX_VALUE / 13 + 1));
+ }
+
+ public void testStringFacadeDoesNotEagerlyAllocateMaximumExpansion() {
+ CDATAEncoder encoder = new CDATAEncoder() {
+ @Override
+ protected int maxEncodedLength(int n) {
+ throw new AssertionError("String facade must grow from actual output");
+ }
+ };
+ String input = "]" + repeat('a', Encode.Buffer.INPUT_BUFFER_SIZE * 4);
+ assertEquals(Encode.forCDATA(input), Encode.encode(encoder, input));
+ }
+
+ public void testHighExpansionRunUsesDocumentedBound() throws Exception {
+ char[] chars = new char[100000];
+ Arrays.fill(chars, ']');
+ String input = new String(chars);
+ String encoded = Encode.forCDATA(input);
+ assertEquals(1300000, encoded.length());
+ assertEquals(encoded.length(), new CDATAEncoder().maxEncodedLength(input.length()));
+
+ StringWriter writer = new StringWriter(encoded.length());
+ Encode.forCDATA(writer, input);
+ assertEquals(encoded, writer.toString());
+ }
+
+ private static String repeat(char ch, int count) {
+ char[] chars = new char[count];
+ Arrays.fill(chars, ch);
+ return new String(chars);
}
}
diff --git a/core/src/test/java/org/owasp/encoder/ContextBoundaryCompositionTest.java b/core/src/test/java/org/owasp/encoder/ContextBoundaryCompositionTest.java
new file mode 100644
index 0000000..822c165
--- /dev/null
+++ b/core/src/test/java/org/owasp/encoder/ContextBoundaryCompositionTest.java
@@ -0,0 +1,434 @@
+// Copyright (c) 2026 OWASP.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+//
+// * Redistributions of source code must retain the above
+// copyright notice, this list of conditions and the following
+// disclaimer.
+//
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following
+// disclaimer in the documentation and/or other materials
+// provided with the distribution.
+//
+// * Neither the name of the OWASP nor the names of its
+// contributors may be used to endorse or promote products
+// derived from this software without specific prior written
+// permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+// OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package org.owasp.encoder;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.nio.CharBuffer;
+import java.nio.charset.CoderResult;
+import java.util.Arrays;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import junit.framework.TestCase;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/** Parser-oracle tests for delimiters split at a trusted/encoded boundary. */
+public class ContextBoundaryCompositionTest extends TestCase {
+
+ public void testJavaScriptEndTagPrefixesRemainScriptData() {
+ String[] delimiters = {">", " ", "\t", "\n", "\f", "\r", "/"};
+ for (String sentinel : scriptEndTagCaseVariants()) {
+ for (String delimiter : delimiters) {
+ String token = sentinel + delimiter
+ + (">".equals(delimiter) ? "" : ">");
+ for (int start = 0; start < token.length(); start++) {
+ for (int end = start + 1; end <= token.length(); end++) {
+ String trusted = token.substring(0, start);
+ String attacker = token.substring(start, end);
+ String trustedSuffix = token.substring(end)
+ + "";
+ String position = sentinel + " " + delimiter + " "
+ + start + ":" + end;
+ assertScriptContained("general " + position, trusted,
+ Encode.forJavaScript(attacker), trustedSuffix);
+ assertScriptContained("block " + position, trusted,
+ Encode.forJavaScriptBlock(attacker), trustedSuffix);
+ }
+ }
+
+ Document negative = Jsoup.parse("");
+ assertEquals("negative control " + sentinel + " " + delimiter,
+ 1, negative.select("img#pwn").size());
+ }
+ }
+ }
+
+ public void testJavaScriptCommentOpenPrefixesAreBroken() {
+ String token = "";
+ for (int start = 0; start < token.length(); start++) {
+ for (int end = start + 1; end <= token.length(); end++) {
+ String trusted = "";
+ assertEquals(Encode.forJavaScript(javascript),
+ lowLevelEncode(Encoders.JAVASCRIPT_ENCODER, javascript, 4));
+ assertEquals(Encode.forJavaScriptBlock(javascript),
+ lowLevelEncode(Encoders.JAVASCRIPT_BLOCK_ENCODER, javascript, 4));
+
+ String cdata = "]]>]>]', "]]>");
+ assertAtomicReplacement(Encoders.XML_COMMENT_ENCODER, '-', "~");
+ }
+
+ public void testEncodedWriterEverySplitAndFlush() throws Exception {
+ assertEncodedWriterSplits(Encoders.JAVASCRIPT, "",
+ Encode.forJavaScript(""));
+ assertEncodedWriterSplits(Encoders.JAVASCRIPT_BLOCK, "",
+ Encode.forJavaScriptBlock(""));
+ assertEncodedWriterSplits(Encoders.CDATA, "]]>]>]>b",
+ Encode.forXmlComment("a--->b"));
+ }
+
+ public void testEncodedWriterFlushDoesNotFinalizeSurrogate() throws Exception {
+ String pair = "\ud83d\ude00";
+ for (String context : new String[] {Encoders.JAVASCRIPT,
+ Encoders.JAVASCRIPT_BLOCK, Encoders.CDATA, Encoders.XML_COMMENT}) {
+ StringWriter output = new StringWriter();
+ EncodedWriter writer = new EncodedWriter(output, context);
+ writer.write(pair.charAt(0));
+ writer.flush();
+ writer.write(pair.charAt(1));
+ writer.close();
+ assertEquals(context, Encode.encode(Encoders.forName(context), pair),
+ output.toString());
+ }
+ }
+
+ public void testCdataTrustedPrefixPreservesTextAndStructure() throws Exception {
+ String token = "]]>";
+ String markup = "";
+ for (int start = 0; start < token.length(); start++) {
+ for (int end = start + 1; end <= token.length(); end++) {
+ String composed = token.substring(0, start)
+ + Encode.forCDATA(token.substring(start, end))
+ + token.substring(end) + markup;
+ org.w3c.dom.Document splitParsed = parseXml(
+ "");
+ assertEquals("CDATA split " + start + ":" + end, 0,
+ splitParsed.getElementsByTagName("evil").getLength());
+ assertEquals("CDATA value " + start + ":" + end, token + markup,
+ splitParsed.getDocumentElement().getTextContent());
+ }
+ }
+
+ String attacker = ">");
+ assertEquals(0, parsed.getElementsByTagName("evil").getLength());
+ assertEquals("]]" + attacker, parsed.getDocumentElement().getTextContent());
+
+ org.w3c.dom.Document raw = parseXml(
+ ""
+ + "");
+ assertEquals("raw CDATA negative control", 1,
+ raw.getElementsByTagName("evil").getLength());
+
+ assertEquals(encoded, writerXml(attacker, Encoders.CDATA));
+ assertEquals(encoded, Encode.encode(Encoders.forName(Encoders.CDATA), attacker));
+ }
+
+ public void testXmlCommentTrustedPrefixCannotCloseComment() throws Exception {
+ String attacker = "->";
+ String encoded = Encode.forXmlComment(attacker);
+ org.w3c.dom.Document parsed = parseXml(
+ "");
+ assertEquals(0, parsed.getElementsByTagName("evil").getLength());
+
+ assertEquals("~x", Encode.forXmlComment("-x"));
+ parseXml("");
+
+ boolean rawFailed = false;
+ try {
+ parseXml("");
+ } catch (SAXParseException expected) {
+ rawFailed = true;
+ }
+ assertTrue("raw trusted-prefix negative control", rawFailed);
+
+ String middle = "";
+ parsed = parseXml(middle);
+ assertEquals(0, parsed.getElementsByTagName("evil").getLength());
+
+ org.w3c.dom.Document rawInjection = parseXml(
+ ""
+ + "");
+ assertEquals("raw comment negative control", 1,
+ rawInjection.getElementsByTagName("evil").getLength());
+
+ String[] forbidden = {"--", "-->"};
+ for (String token : forbidden) {
+ for (int start = 0; start <= 1; start++) {
+ for (int end = start + 1; end <= token.length(); end++) {
+ String comment = token.substring(0, start)
+ + Encode.forXmlComment(token.substring(start, end))
+ + token.substring(end) + "";
+ parsed = parseXml("");
+ assertEquals(token + " " + start + ":" + end, 0,
+ parsed.getElementsByTagName("evil").getLength());
+ }
+ }
+ }
+
+ assertEquals(encoded, writerXml(attacker, Encoders.XML_COMMENT));
+ assertEquals(encoded,
+ Encode.encode(Encoders.forName(Encoders.XML_COMMENT), attacker));
+ }
+
+ private static void assertScriptContained(String message, String trusted,
+ String encoded, String trustedSuffix) {
+ Document document = Jsoup.parse("
after
");
+ assertEquals(message, 0, document.select("img#pwn").size());
+ assertEquals(message, 1, document.select("script").size());
+ assertEquals(message, "after", document.getElementById("after").text());
+ }
+
+ private static String writerJavaScript(String input, boolean block)
+ throws Exception {
+ StringWriter output = new StringWriter();
+ if (block) {
+ Encode.forJavaScriptBlock(output, input);
+ } else {
+ Encode.forJavaScript(output, input);
+ }
+ return output.toString();
+ }
+
+ private static String writerXml(String input, String context) throws Exception {
+ StringWriter output = new StringWriter();
+ if (Encoders.CDATA.equals(context)) {
+ Encode.forCDATA(output, input);
+ } else {
+ Encode.forXmlComment(output, input);
+ }
+ return output.toString();
+ }
+
+ private static String[] scriptEndTagCaseVariants() {
+ String letters = "script";
+ String[] variants = new String[1 << letters.length()];
+ for (int mask = 0; mask < variants.length; mask++) {
+ StringBuilder value = new StringBuilder("");
+ for (int i = 0; i < letters.length(); i++) {
+ char ch = letters.charAt(i);
+ value.append((mask & (1 << i)) == 0 ? ch : Character.toUpperCase(ch));
+ }
+ variants[mask] = value.toString();
+ }
+ return variants;
+ }
+
+ private static String lowLevelEncode(Encoder encoder, String value, int outputCapacity) {
+ char[] inputArray = new char[value.length() + 4];
+ Arrays.fill(inputArray, '^');
+ value.getChars(0, value.length(), inputArray, 2);
+ CharBuffer inputParent = CharBuffer.wrap(inputArray);
+ inputParent.position(1).limit(value.length() + 3);
+ CharBuffer input = inputParent.slice();
+ input.position(1).limit(value.length() + 1);
+
+ StringBuilder encoded = new StringBuilder();
+ while (true) {
+ char[] outputArray = new char[outputCapacity + 4];
+ Arrays.fill(outputArray, '^');
+ CharBuffer outputParent = CharBuffer.wrap(outputArray);
+ outputParent.position(1).limit(outputCapacity + 3);
+ CharBuffer output = outputParent.slice();
+ output.position(1).limit(outputCapacity + 1);
+ int previousInputPosition = input.position();
+
+ CoderResult result = encoder.encode(input, output, true);
+ encoded.append(outputArray, 2, output.position() - 1);
+ assertEquals("leading output sentinel", '^', outputArray[0]);
+ assertEquals("leading output sentinel", '^', outputArray[1]);
+ assertEquals("trailing output sentinel", '^',
+ outputArray[output.position() + 1]);
+
+ if (result.isUnderflow()) {
+ assertFalse(input.hasRemaining());
+ break;
+ }
+ assertTrue(result.isOverflow());
+ assertTrue("overflow must make progress with an atomic-size buffer",
+ input.position() > previousInputPosition);
+ }
+ assertEquals("leading input sentinel", '^', inputArray[0]);
+ assertEquals("leading input sentinel", '^', inputArray[1]);
+ assertEquals("trailing input sentinel", '^', inputArray[inputArray.length - 1]);
+ return encoded.toString();
+ }
+
+ private static void assertAtomicReplacement(Encoder encoder, char value,
+ String expected) {
+ int[] capacities = {expected.length() - 1, expected.length(),
+ expected.length() + 1};
+ for (int capacity : capacities) {
+ char[] inputArray = {'^', '^', '^', value, '^', '^'};
+ CharBuffer inputParent = CharBuffer.wrap(inputArray);
+ inputParent.position(2).limit(5);
+ CharBuffer input = inputParent.slice();
+ input.position(1).limit(2);
+
+ char[] outputArray = new char[capacity + 6];
+ Arrays.fill(outputArray, '^');
+ CharBuffer outputParent = CharBuffer.wrap(outputArray);
+ outputParent.position(2).limit(capacity + 4);
+ CharBuffer output = outputParent.slice();
+ output.position(1).limit(capacity + 1);
+
+ CoderResult result = encoder.encode(input, output, true);
+ if (capacity < expected.length()) {
+ assertTrue(encoder.toString(), result.isOverflow());
+ assertEquals(1, input.position());
+ assertEquals(1, output.position());
+ } else {
+ assertTrue(encoder.toString(), result.isUnderflow());
+ assertEquals(2, input.position());
+ assertEquals(expected.length() + 1, output.position());
+ assertEquals(expected, new String(outputArray, 3, expected.length()));
+ }
+
+ int written = capacity < expected.length() ? 0 : expected.length();
+ for (int i = 0; i < outputArray.length; i++) {
+ if (i < 3 || i >= 3 + written) {
+ assertEquals("sentinel " + encoder + " capacity " + capacity
+ + " index " + i, '^', outputArray[i]);
+ }
+ }
+ }
+ }
+
+ private static void assertEncodedWriterSplits(String context, String input,
+ String expected) throws Exception {
+ for (int split = 0; split <= input.length(); split++) {
+ StringWriter output = new StringWriter();
+ EncodedWriter writer = new EncodedWriter(output, context);
+ writer.write(input, 0, split);
+ writer.flush();
+ writer.write(input, split, input.length() - split);
+ writer.close();
+ assertEquals(context + " split " + split, expected, output.toString());
+ }
+
+ StringWriter output = new StringWriter();
+ EncodedWriter writer = new EncodedWriter(output, context);
+ for (int i = 0; i < input.length(); i++) {
+ writer.write(input.charAt(i));
+ writer.flush();
+ }
+ writer.close();
+ assertEquals(context + " one-character chunks", expected, output.toString());
+ }
+
+ private static org.w3c.dom.Document parseXml(String xml) throws Exception {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ builder.setErrorHandler(new DefaultHandler() {
+ @Override
+ public void error(SAXParseException exception) throws SAXParseException {
+ throw exception;
+ }
+
+ @Override
+ public void fatalError(SAXParseException exception)
+ throws SAXParseException {
+ throw exception;
+ }
+ });
+ return builder.parse(new InputSource(new StringReader(xml)));
+ }
+}
diff --git a/core/src/test/java/org/owasp/encoder/EncodedWriterTest.java b/core/src/test/java/org/owasp/encoder/EncodedWriterTest.java
index eecc074..d610e79 100644
--- a/core/src/test/java/org/owasp/encoder/EncodedWriterTest.java
+++ b/core/src/test/java/org/owasp/encoder/EncodedWriterTest.java
@@ -36,6 +36,7 @@
import java.io.IOException;
import java.io.StringWriter;
+import java.io.Writer;
import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.Arrays;
@@ -104,6 +105,199 @@ public void pendingLookaheadWithoutMoreInputDoesNotHang() throws IOException {
assertEquals("x\uD83D\uDE00", html.toString());
}
+ @Test
+ public void closeIsIdempotentAndClosedOperationsFail() throws IOException {
+ StrictWriter out = new StrictWriter();
+ final EncodedWriter writer = new EncodedWriter(out, Encoders.HTML);
+ writer.write("a\uD83D");
+ writer.close();
+ assertEquals("a ", out.toString());
+ assertEquals(1, out.closeCount);
+
+ writer.close();
+ assertEquals(1, out.closeCount);
+ assertIOException(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.write('z');
+ }
+ });
+ assertIOException(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.write(new char[0], 0, 0);
+ }
+ });
+ assertIOException(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.write(new char[] {'z'});
+ }
+ });
+ assertIOException(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.write("z");
+ }
+ });
+ assertIOException(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.write("z", 0, 1);
+ }
+ });
+ assertIOException(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.append('z');
+ }
+ });
+ assertIOException(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.append("z");
+ }
+ });
+ assertIOException(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.append("xyz", 1, 2);
+ }
+ });
+ assertIOException(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.flush();
+ }
+ });
+ }
+
+ @Test
+ public void closeStillClosesDelegateWhenFinalWriteFails() throws IOException {
+ FailingWriter out = new FailingWriter(true, false);
+ EncodedWriter writer = new EncodedWriter(out, Encoders.HTML);
+ writer.write('<');
+
+ try {
+ writer.close();
+ fail("close should propagate the final write failure");
+ } catch (IOException expected) {
+ assertEquals("write failed", expected.getMessage());
+ }
+ assertEquals(1, out.closeCount);
+
+ writer.close();
+ assertEquals(1, out.closeCount);
+ }
+
+ @Test
+ public void closePreservesSuppressedDelegateFailure() throws IOException {
+ FailingWriter out = new FailingWriter(true, true);
+ EncodedWriter writer = new EncodedWriter(out, Encoders.HTML);
+ writer.write('<');
+
+ try {
+ writer.close();
+ fail("close should propagate the final write failure");
+ } catch (IOException expected) {
+ assertEquals("write failed", expected.getMessage());
+ assertEquals(1, expected.getSuppressed().length);
+ assertEquals("close failed", expected.getSuppressed()[0].getMessage());
+ }
+ assertEquals(1, out.closeCount);
+ }
+
+ @Test
+ public void closePropagatesDelegateCloseFailureAndBecomesIdempotent()
+ throws IOException {
+ FailingWriter out = new FailingWriter(false, true);
+ EncodedWriter writer = new EncodedWriter(out, Encoders.HTML);
+ writer.write('a');
+
+ try {
+ writer.close();
+ fail("close should propagate the delegate close failure");
+ } catch (IOException expected) {
+ assertEquals("close failed", expected.getMessage());
+ assertEquals(0, expected.getSuppressed().length);
+ }
+ assertEquals(1, out.closeCount);
+ writer.close();
+ assertEquals(1, out.closeCount);
+ }
+
+ @Test
+ public void flushDoesNotFinalizePendingInput() throws IOException {
+ StringWriter out = new StringWriter();
+ EncodedWriter writer = new EncodedWriter(out, Encoders.HTML);
+ writer.write("x\uD83D");
+ writer.flush();
+ assertEquals("x", out.toString());
+ writer.write("\uDE00");
+ writer.close();
+ assertEquals("x\uD83D\uDE00", out.toString());
+ }
+
+ @Test
+ public void invalidWriteRangesThrowIndexOutOfBoundsException() throws IOException {
+ StringWriter out = new StringWriter();
+ final EncodedWriter writer = new EncodedWriter(out, Encoders.HTML);
+ final char[] chars = {'a'};
+
+ try {
+ writer.write((char[]) null, 0, 0);
+ fail("expected NullPointerException");
+ } catch (NullPointerException expected) {
+ // Expected.
+ }
+ assertIndexOutOfBounds(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.write(chars, -1, 1);
+ }
+ });
+ assertIndexOutOfBounds(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.write(chars, 0, -1);
+ }
+ });
+ assertIndexOutOfBounds(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.write(chars, 1, 1);
+ }
+ });
+ assertIndexOutOfBounds(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.write(chars, Integer.MAX_VALUE, 1);
+ }
+ });
+ assertIndexOutOfBounds(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.write(chars, 1, Integer.MAX_VALUE);
+ }
+ });
+ assertIndexOutOfBounds(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.write(chars, Integer.MAX_VALUE, Integer.MAX_VALUE);
+ }
+ });
+ assertIndexOutOfBounds(new IOAction() {
+ @Override
+ public void run() throws IOException {
+ writer.write(chars, chars.length + 1, 0);
+ }
+ });
+
+ writer.write(chars, chars.length, 0);
+ writer.close();
+ assertEquals("", out.toString());
+ }
+
@Test(timeout = 60000)
public void splitWritesMatchSingleCallEncoding() throws Exception {
List names = MaxEncodedLengthTest.contextNames();
@@ -184,4 +378,93 @@ private static String repeat(char ch, int n) {
Arrays.fill(chars, ch);
return new String(chars);
}
+
+ private static void assertIOException(IOAction action) {
+ try {
+ action.run();
+ fail("expected IOException");
+ } catch (IOException expected) {
+ // Expected.
+ }
+ }
+
+ private static void assertIndexOutOfBounds(IOAction action) throws IOException {
+ try {
+ action.run();
+ fail("expected IndexOutOfBoundsException");
+ } catch (IndexOutOfBoundsException expected) {
+ // Expected.
+ }
+ }
+
+ private interface IOAction {
+ void run() throws IOException;
+ }
+
+ private static final class StrictWriter extends Writer {
+
+ private final StringBuilder output = new StringBuilder();
+ private boolean closed;
+ private int closeCount;
+
+ @Override
+ public void write(char[] cbuf, int off, int len) throws IOException {
+ ensureOpen();
+ output.append(cbuf, off, len);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ ensureOpen();
+ }
+
+ @Override
+ public void close() {
+ closed = true;
+ ++closeCount;
+ }
+
+ @Override
+ public String toString() {
+ return output.toString();
+ }
+
+ private void ensureOpen() throws IOException {
+ if (closed) {
+ throw new IOException("strict writer closed");
+ }
+ }
+ }
+
+ private static final class FailingWriter extends Writer {
+
+ private final boolean failWrite;
+ private final boolean failClose;
+ private int closeCount;
+
+ FailingWriter(boolean failWrite, boolean failClose) {
+ this.failWrite = failWrite;
+ this.failClose = failClose;
+ }
+
+ @Override
+ public void write(char[] cbuf, int off, int len) throws IOException {
+ if (failWrite) {
+ throw new IOException("write failed");
+ }
+ }
+
+ @Override
+ public void flush() {
+ // Nothing to do.
+ }
+
+ @Override
+ public void close() throws IOException {
+ ++closeCount;
+ if (failClose) {
+ throw new IOException("close failed");
+ }
+ }
+ }
}
diff --git a/core/src/test/java/org/owasp/encoder/JavaScriptEncoderTest.java b/core/src/test/java/org/owasp/encoder/JavaScriptEncoderTest.java
index fefe3fd..00694f9 100644
--- a/core/src/test/java/org/owasp/encoder/JavaScriptEncoderTest.java
+++ b/core/src/test/java/org/owasp/encoder/JavaScriptEncoderTest.java
@@ -52,7 +52,18 @@ public static Test suite() {
for (int asciiOnly = 0 ; asciiOnly <= 1 ; ++asciiOnly) {
for (JavaScriptEncoder.Mode mode : JavaScriptEncoder.Mode.values()) {
// if (!(mode == JavaScriptEncoder.Mode.HTML_CONTENT && asciiOnly == 0)) continue;
- EncoderTestSuiteBuilder builder = new EncoderTestSuiteBuilder(new JavaScriptEncoder(mode, asciiOnly==1), "(safe)", "(\\)")
+ boolean htmlBlock = mode == Mode.BLOCK || mode == Mode.HTML;
+ String trustedDollarExpected = htmlBlock
+ ? "\\x7bexe\\x63u\\x74ed=\\x74\\x72ue}"
+ : "\\x7bexecuted=true}";
+ String templateExpressionExpected = htmlBlock
+ ? "\\x24\\x7bale\\x72\\x74(1)}"
+ : "\\x24\\x7balert(1)}";
+ String templateBreakoutExpected = htmlBlock
+ ? "hell\\x60;ale\\x72\\x74(1);\\x60o"
+ : "hell\\x60;alert(1);\\x60o";
+ EncoderTestSuiteBuilder builder = new EncoderTestSuiteBuilder(
+ new JavaScriptEncoder(mode, asciiOnly==1), "(xyz)", "(\\)")
.encoded(0, 0x1f)
.valid(' ', '~')
.encoded("\\\'\"`${");
@@ -78,9 +89,17 @@ public static Test suite() {
case BLOCK:
case HTML:
builder
+ .encode("\\x20", " ")
+ .encode("\\x21", "!")
.encode("\\/", "/")
- .encode("\\-", "-")
- .encoded("/-");
+ .encode("\\x2d", "-")
+ .encode("\\x3c", "<")
+ .encode("\\x3e", ">")
+ .encode("script delimiter alphabet",
+ "\\x43\\x49\\x50\\x52\\x53\\x54"
+ + "\\x63\\x69\\x70\\x72\\x73\\x74",
+ "CIPRSTciprst")
+ .encoded(" !-/<>CIPRSTciprst");
break;
default:
builder.encode("/", "/");
@@ -108,14 +127,17 @@ public static Test suite() {
.encode("dollar", "\\x24", "$")
.encode("opening brace", "\\x7b", "{")
.encode("template start", "\\x24\\x7b", "${")
- .encode("trusted dollar boundary", "\\x7bexecuted=true}", "{executed=true}")
+ .encode("trusted dollar boundary", trustedDollarExpected,
+ "{executed=true}")
.encode("trailing dollar", "end\\x24", "end$")
.encode("escaped-looking interpolation", "\\\\\\x24\\x7bvalue}", "\\${value}")
.encode("escaped-looking backtick", "\\\\\\x60", "\\`")
- .encode("template expression", "\\x24\\x7balert(1)}", "${alert(1)}")
- .encode("template breakout", "hell\\x60;alert(1);\\x60o", "hell`;alert(1);`o")
- .encode("abc", "abc")
- .encode("ABC", "ABC")
+ .encode("template expression", templateExpressionExpected,
+ "${alert(1)}")
+ .encode("template breakout", templateBreakoutExpected,
+ "hell`;alert(1);`o")
+ .encode("abc", htmlBlock ? "ab\\x63" : "abc", "abc")
+ .encode("ABC", htmlBlock ? "AB\\x43" : "ABC", "ABC")
// DEL and the C1 controls are hex encoded in every mode
.encode("DEL", "\\x7f", "\u007f")
.encode("U+0080", "\\x80", "\u0080")
@@ -155,18 +177,24 @@ public static Test suite() {
public void testTemplateCharactersThroughPublicFacadesAndRegistry() throws Exception {
String input = "price=$5;`${value}`;\\${escaped}";
String expected = "price=\\x245;\\x60\\x24\\x7bvalue}\\x60;\\\\\\x24\\x7bescaped}";
+ String htmlBlockExpected = "\\x70\\x72\\x69\\x63e=\\x245;"
+ + "\\x60\\x24\\x7bvalue}\\x60;\\\\\\x24\\x7be\\x73\\x63a\\x70ed}";
+ String trustedDollarExpected = "\\x7bexe\\x63u\\x74ed=\\x74\\x72ue}";
String[] methods = {"forJavaScript", "forJavaScriptAttribute",
"forJavaScriptBlock", "forJavaScriptSource"};
String[] contexts = {Encoders.JAVASCRIPT, Encoders.JAVASCRIPT_ATTRIBUTE,
Encoders.JAVASCRIPT_BLOCK, Encoders.JAVASCRIPT_SOURCE};
for (int i = 0; i < methods.length; i++) {
- assertEquals(methods[i], expected,
+ String methodExpected = (i == 0 || i == 2) ? htmlBlockExpected : expected;
+ assertEquals(methods[i], methodExpected,
Encode.class.getMethod(methods[i], String.class).invoke(null, input));
StringWriter out = new StringWriter();
Encode.class.getMethod(methods[i], Writer.class, String.class).invoke(null, out, input);
- assertEquals(methods[i], expected, out.toString());
- assertEquals(contexts[i], expected, Encode.encode(Encoders.forName(contexts[i]), input));
- assertEquals(methods[i], "\\x7bexecuted=true}",
+ assertEquals(methods[i], methodExpected, out.toString());
+ assertEquals(contexts[i], methodExpected,
+ Encode.encode(Encoders.forName(contexts[i]), input));
+ assertEquals(methods[i], (i == 0 || i == 2)
+ ? trustedDollarExpected : "\\x7bexecuted=true}",
Encode.class.getMethod(methods[i], String.class).invoke(null, "{executed=true}"));
}
}
diff --git a/core/src/test/java/org/owasp/encoder/XMLCommentEncoderTest.java b/core/src/test/java/org/owasp/encoder/XMLCommentEncoderTest.java
index 4239caa..f8d5094 100644
--- a/core/src/test/java/org/owasp/encoder/XMLCommentEncoderTest.java
+++ b/core/src/test/java/org/owasp/encoder/XMLCommentEncoderTest.java
@@ -46,7 +46,8 @@ public class XMLCommentEncoderTest extends TestCase {
public static Test suite() {
return new EncoderTestSuiteBuilder(
XMLCommentEncoderTest.class, new XMLCommentEncoder(), "(safe)", "--")
- .encode("a - b", "a - b")
+ .encode("a ~ b", "a - b")
+ .encode("trusted-prefix boundary", "~>", "->")
.encode("<\"&\'>", "<\"&\'>") // valid in comments, not in XML
.encode("missing-low-surrogate", " x", "\ud800x")
@@ -85,8 +86,9 @@ XMLCommentEncoderTest.class, new XMLCommentEncoder(), "(safe)", "--")
public void testEncodeHyphens() throws Exception {
XMLCommentEncoder encoder = new XMLCommentEncoder();
- assertEquals("-~", Encode.encode(encoder, "--"));
- assertEquals("ab-~cd", Encode.encode(encoder, "ab--cd"));
+ assertEquals("~~", Encode.encode(encoder, "--"));
+ assertEquals("ab~~cd", Encode.encode(encoder, "ab--cd"));
+ assertEquals("ab~>cd", Encode.encode(encoder, "ab->cd"));
}
public void testEncodeHyphenAtEnd() throws Exception {
@@ -97,6 +99,6 @@ public void testEncodeHyphenAtEnd() throws Exception {
public void testEncodeHyphenBar() throws Exception {
XMLCommentEncoder encoder = new XMLCommentEncoder();
- assertEquals("-~-~-~-~-~~", Encode.encode(encoder, "-----------"));
+ assertEquals("~~~~~~~~~~~", Encode.encode(encoder, "-----------"));
}
}
diff --git a/docs/compatibility-decisions.md b/docs/compatibility-decisions.md
index 538f01c..0e86b60 100644
--- a/docs/compatibility-decisions.md
+++ b/docs/compatibility-decisions.md
@@ -40,6 +40,24 @@ are already merged 1.5 features; they are not deferred or rejected by this recor
Tagged templates, arbitrary executable expressions and complete JSON serialization
remain outside those contracts.
+## 1.5 parser-boundary output migration
+
+The 1.5 security correction keeps the existing trusted-prefix/encoded-value/
+trusted-suffix composition contract. Applications do not need a new stateful
+API, but three outputs change compared with 1.4.1:
+
+| Context | 1.5 behavior | Compatibility consequence |
+| --- | --- | --- |
+| HTML script JavaScript (`forJavaScript`, `forJavaScriptBlock`) | Escapes every character that can contribute to a case-insensitive ``; hyphen uses `\x2d` | JavaScript string values are preserved, but common letters and spaces can change encoded bytes. Attribute-only and standalone-source modes retain their narrower rules. |
+| XML CDATA | Represents every `]` and `>` with a close/character-data/reopen spelling | Parsed XML text is preserved, apart from documented XML normalization/replacement. Output can expand 13× and parser event boundaries can change. |
+| XML comment | Replaces every hyphen with `~` | Prevents cross-fragment `--` and `-->`; comment text is deliberately lossy. |
+
+The trusted fragments must already be valid for the selected parser context.
+Migration review must cover byte snapshots, signatures, cache keys and any code
+that consumes XML events rather than the parsed text value. Large CDATA values
+should use a Writer facade or `EncodedWriter`; String facades retain the complete
+result but grow according to actual output rather than reserving the 13× bound.
+
## Base64url disposition (#149)
**Reject adding a Base64Url class, `Encode.forBase64Url`, or tag/EL bindings to this
diff --git a/docs/contexts.md b/docs/contexts.md
index 11a1a04..e3a4844 100644
--- a/docs/contexts.md
+++ b/docs/contexts.md
@@ -35,6 +35,9 @@ pending input. Use the facade Writer overload when encoding one String directly.
parser. Some XML 1.1 control-character references (such as ``) are invalid
in XML 1.0; do not use XML encoders for HTML.
`forCDATA` and `forXmlComment` implement XML 1.0 contexts; supply their delimiters.
+ To remain safe next to trusted literal text, CDATA represents every `]` and
+ `>` with close/reopen sequences, and XML comments replace every input hyphen
+ with `~`.
Invalid-character replacement and XML line-ending normalization can change data.
- Java source: `forJava` encodes string-literal content for a code generator,
not JavaScript or JSON. Supply Java quotes; malformed surrogate input is not
@@ -66,6 +69,24 @@ escapes, preserving JavaScript string values through UTF-8 output. Valid surroga
pairs remain intact. This does not promise that every downstream system accepts
unpaired surrogates.
+The general and block encoders also escape every ASCII character that can form
+part of a case-insensitive `` script
+tokens. This keeps any nonempty encoded substring from completing a delimiter
+supplied partly by adjacent trusted literal string text. The additional output
+changes include space, `!`, hyphen (`\x2d`), `<`, `>`, slash, and both cases of
+the letters in `script`. JavaScript interprets these spellings as the original
+string value, but byte-for-byte output, snapshots, signatures and cache keys can
+change. `forJavaScriptAttribute` and `forJavaScriptSource` do not apply the HTML
+raw-text rule because their output is not for an HTML script element.
+
+CDATA has the same fragment-composition guarantee for `]]>` and preserves the
+XML parser's text value, subject to the existing invalid-character replacement
+and line-ending normalization policies. Its output can expand to thirteen
+characters for each input `]` or `>` and may be reported as multiple CDATA/text
+events. XML-comment encoding is deliberately lossy: every input hyphen becomes
+`~`. Trusted text on either side must itself be legal in the selected context;
+the guarantee prevents the encoded fragment from completing a parser token.
+
## JSON string content — new in 1.5
`Encode.forJson` encodes **one JSON string's content**. The caller supplies the
diff --git a/docs/dependencies.md b/docs/dependencies.md
index 1f0ec71..27ecc08 100644
--- a/docs/dependencies.md
+++ b/docs/dependencies.md
@@ -3,13 +3,14 @@
The core `encoder` has **no runtime dependencies**. `encoder-jsp` and
`encoder-jakarta-jsp` depend on core and declare their matching JSP API as
`provided`; the container supplies it. `encoder-esapi` has compile dependencies
-on core and ESAPI 2.7.0.0, including ESAPI's transitive graph. None of these
-third-party classes is shaded into the four Encoder JARs. The optional Boot/WAR
+on core, ESAPI 2.7.0.0, and patched HTTP Components, including ESAPI's other
+transitives. None of these third-party classes is shaded into the four Encoder
+JARs. The optional Boot/WAR
fixture, test engines and build plugins are development tooling, not published
library runtime dependencies. Review the [live dependency graph](https://github.com/OWASP/owasp-java-encoder/network/dependencies)
for those separate scopes and [ESAPI advisory triage](../esapi/README.md#dependency-security-triage).
-## Consumer dependency inventory — 2026-09-26
+## Consumer dependency inventory — 2026-09-27
Generated from dependency-plugin 3.11.0's resolved reactor `dependency:tree`
JSON for current `1.5.0-SNAPSHOT`. Includes compile/runtime and provided scopes,
@@ -42,9 +43,9 @@ security support or advisory status. This inventory does not rewrite published
| `javax.servlet.jsp:javax.servlet.jsp-api:2.2.1` | jsp: provided | CDDL + GPLv2 with classpath exception | [POM](https://repo.maven.apache.org/maven2/javax/servlet/jsp/javax.servlet.jsp-api/2.2.1/javax.servlet.jsp-api-2.2.1.pom) |
| `org.apache-extras.beanshell:bsh:2.0b6` | esapi: compile | Apache License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache-extras/beanshell/bsh/2.0b6/bsh-2.0b6.pom) |
| `org.apache.commons:commons-collections4:4.5.0-M2` | esapi: compile | Apache-2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/32/apache-32.pom) |
-| `org.apache.httpcomponents.client5:httpclient5:5.4.4` | esapi: compile | Apache License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/27/apache-27.pom) |
-| `org.apache.httpcomponents.core5:httpcore5:5.3.4` | esapi: compile | Apache License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/27/apache-27.pom) |
-| `org.apache.httpcomponents.core5:httpcore5-h2:5.3.4` | esapi: compile | Apache License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/27/apache-27.pom) |
+| `org.apache.httpcomponents.client5:httpclient5:5.6.4` | esapi: compile | Apache License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/27/apache-27.pom) |
+| `org.apache.httpcomponents.core5:httpcore5:5.4.4` | esapi: compile | Apache License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/27/apache-27.pom) |
+| `org.apache.httpcomponents.core5:httpcore5-h2:5.4.4` | esapi: compile | Apache License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/27/apache-27.pom) |
| `org.apache.xmlgraphics:batik-constants:1.19` | esapi: compile | The Apache Software License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/xmlgraphics/batik/1.19/batik-1.19.pom) |
| `org.apache.xmlgraphics:batik-css:1.19` | esapi: compile | The Apache Software License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/xmlgraphics/batik/1.19/batik-1.19.pom) |
| `org.apache.xmlgraphics:batik-i18n:1.19` | esapi: compile | The Apache Software License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/xmlgraphics/batik/1.19/batik-1.19.pom) |
diff --git a/docs/usage.md b/docs/usage.md
index 0dd5697..5660dac 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -21,6 +21,41 @@ String-returning calls such as `Encode.forHtmlAttribute(userText)` have the same
context contract. Do not reuse this result for JavaScript, CSS or a URL component.
Avoid double escaping with frameworks that already escape HTML output.
+## Composing trusted and untrusted fragments — changed in 1.5
+
+The 1.5 JavaScript block, CDATA and XML-comment encoders protect the boundary on
+both sides of each nonempty encoded fragment. Ordinary composition remains
+supported:
+
+```java
+out.write("");
+
+out.write("");
+
+out.write("");
+```
+
+The trusted prefix and suffix must themselves be valid syntax. Keep the encoder
+matched to the actual parser: `forJavaScriptBlock` and the general
+`forJavaScript` protect HTML script raw text; `forJavaScriptAttribute` is for a
+quoted event-handler attribute, and `forJavaScriptSource` is for a standalone
+resource.
+
+This correction changes emitted text. HTML-script JavaScript uses additional
+hex escapes (including `\x2d` for hyphen and escapes for both cases of the
+letters in `script`) while preserving the JavaScript string value. CDATA
+preserves parsed XML text but can expand each `]` or `>` to 13 characters and
+can change SAX/StAX event boundaries. XML-comment encoding replaces every
+hyphen with `~` and therefore does not preserve comment text. Review snapshots,
+signatures, cache keys and code that inspects encoded bytes. Prefer Writer APIs
+for large or high-expansion CDATA values.
+
## URLs
For a same-origin search URL with a fixed trusted path and one raw query value:
diff --git a/esapi/README.md b/esapi/README.md
index 6d6613b..05e6640 100644
--- a/esapi/README.md
+++ b/esapi/README.md
@@ -6,10 +6,10 @@ The ESAPI dependency depends on the `encoder-esapi` release you consume:
| --- | --- | --- |
| `1.4.0` | Maven range `[2.5.1.0,3)`; resolution can change and can select a release candidate | Maven Central; affected by Java Encoder's 1.4.1 security advisories |
| `1.4.1` | Fixed default `2.7.0.0` | Maven Central and [signed GitHub security release][encoder-release] |
-| `1.5.0-SNAPSHOT` | Fixed default `2.7.0.0` | Unreleased development; not a published release |
+| `1.5.0-SNAPSHOT` | Fixed default `2.7.0.0`, plus patched HTTP Components compile dependencies | Unreleased development; not a published release |
The fixed dependency was introduced in 1.4.1. It does not change the POM already
-published for 1.4.0. As checked on 2026-09-25, upstream's [latest stable release][esapi-latest]
+published for 1.4.0. As checked on 2026-09-27, upstream's [latest stable release][esapi-latest]
and [security policy][esapi-security] identify ESAPI 2.7.0.0 as current and supported.
Recheck those sources when choosing a version; adapter compatibility does not
establish upstream security support.
@@ -149,14 +149,19 @@ configuration to select the implementation.
The ESAPI dependency remains a compile dependency because its `Encoder` type is
part of the adapter's public API. Its transitive dependencies are therefore also
-available to applications. The ESAPI module enforces dependency convergence for
-this graph, but applications should continue to scan their complete resolved
-graph because their other dependencies may change Maven conflict resolution.
+available to applications. The unreleased 1.5 POM additionally declares
+`httpclient5:5.6.4`, `httpcore5:5.4.4`, and `httpcore5-h2:5.4.4` as direct compile
+dependencies. These stable versions replace AntiSamy 1.7.8's older HTTP
+transitives in a standalone Maven consumer. Dependency management aligns the
+adapter's own graph for convergence; management alone did not carry those
+versions into a consumer's graph. Applications should scan their complete
+resolved graph because their own dependencies or BOMs can change resolution.
The [ESAPI 2.7.0.0 release][esapi-release] addresses CVE-2025-5878 and updates
transitive dependencies for CVE-2025-48976 and CVE-2025-48734. Its upstream POM
intentionally depends on the milestone `commons-collections4` 4.5.0-M2; this
-adapter does not override ESAPI's tested graph.
+adapter retains that selection. The targeted HTTP overrides do not change the
+ESAPI or AntiSamy release.
### Dependency security triage
@@ -164,37 +169,42 @@ The resolved dependency submissions and [Dependabot alerts][dependency-alerts]
are the ongoing inventory, including transitive runtime/test dependencies and
build plugins. Review the actual version, path and execution scope of each new
alert; the fact that ESAPI introduces a dependency is not a dismissal reason.
-On 2026-09-26 UTC, Maven Central still had 2.7.0.0 as the newest stable ESAPI
-release (2.7.0.1-RC1 was a prerelease). The default graph includes these findings:
-
-- Commons Configuration 1.10: [GHSA-pvp8-3xj6-8c6x][]; no patched 1.x release
-- Commons Lang 2.6: [GHSA-j288-q9x7-2f5v][]; no patched 2.x release
-- HttpClient 5.4.4: [GHSA-hjcp-jmpx-g3qm][]; patched in 5.6.3
-- HttpCore and HttpCore H2 5.3.4: [GHSA-hf6x-8p5f-cgmf][] and
- [GHSA-v3jc-474w-2wm6][]; patched in 5.4.3
-
-The Commons Configuration finding concerns resource use while loading untrusted
-configuration; delegated ESAPI calls initialize reference configuration, so keep
-that configuration trusted. Commons Lang's finding concerns attacker-controlled
-class names passed to `ClassUtils.getClass`. HttpClient's finding concerns classic
-HTTP response decoding and connection release; HttpCore's findings concern HTTP/1
-header parsing and HTTP/2 HPACK decoding. The adapter's Java Encoder-backed
-methods perform string encoding without these HTTP or configuration operations.
-Delegated methods and applications using other ESAPI/AntiSamy features have a
-different scope, so this is not a blanket application reachability conclusion.
-
-Disposition: retain these findings for upstream/application assessment; no
-blanket suppression or untested POM override is applied. Prefer a stable upstream
-ESAPI release with a tested fixed graph. If it is unavailable, a mitigation or
-override may be accepted after review of the affected feature's reachability,
-API/runtime compatibility, dependency convergence, and the complete adapter and
-packaged-consumer matrix. Commons Configuration 2.x and Commons Lang 3.x use
-different APIs/namespaces and cannot silently replace the legacy coordinates.
-The maintainer team owns this triage. Record the advisory, affected versions,
-scope, evidence, owner and recheck date
-for any exception; time-limit suppressions and reopen them when assumptions
-change. Recheck this disposition on the next ESAPI release or within 90 days.
-Do not close an alert simply because it is transitive or adapter tests pass.
+As checked on 2026-09-27, ESAPI 2.7.0.0 remained the newest stable upstream
+release (2.7.0.1-RC1 was a prerelease). The resolved compile graph still has two
+findings, both introduced through `encoder-esapi -> esapi:2.7.0.0`:
+
+- `commons-configuration:commons-configuration:1.10` (compile):
+ [GHSA-pvp8-3xj6-8c6x][]. The trigger is loading untrusted configuration or
+ attacker-controlled usage patterns, which can consume excessive resources.
+ Delegated ESAPI calls can initialize reference configuration; keep it trusted.
+ There is no patched 1.x release.
+- `commons-lang:commons-lang:2.6` (compile): [GHSA-j288-q9x7-2f5v][]. The
+ trigger is a very long attacker-controlled class name passed to
+ `ClassUtils.getClass`, which can cause uncontrolled recursion. There is no
+ patched 2.x release.
+
+The adapter's Java Encoder-backed methods perform string encoding without
+these configuration or class-lookup operations. Delegated methods and
+applications using ESAPI's other features require their own reachability
+assessment. Commons Configuration 2.x and Commons Lang 3.x use different
+coordinates/APIs and cannot silently replace these legacy dependencies.
+
+The previous HTTP findings are absent from the 1.5 consumer compile graph:
+`httpclient5:5.6.4` is beyond the patch for [GHSA-hjcp-jmpx-g3qm][], and
+`httpcore5:5.4.4` / `httpcore5-h2:5.4.4` are beyond the patches for
+[GHSA-hf6x-8p5f-cgmf][] / [GHSA-v3jc-474w-2wm6][]. These advisories concern
+classic HTTP response decoding and connection release, HTTP/1 header parsing,
+and HTTP/2 HPACK decoding respectively. They remain relevant to older adapter
+releases or applications that independently resolve affected HTTP versions.
+
+Residual disposition, owner: the OWASP Java Encoder maintainer team retains
+the two Commons findings for upstream/application assessment, without
+suppression. Recheck by **2026-12-26** (90 days from this review) or at the next
+stable ESAPI release, whichever comes first. Review any new upstream fix for
+API/runtime compatibility, dependency convergence, adapter tests, and packaged
+consumers before changing the graph. Record the advisory, resolved path and
+scope, trigger, evidence, owner, and recheck date for any application exception.
+Do not close an alert solely because it is transitive or adapter tests pass.
ESAPI 2.7 disables `encodeForSQL` by default. The adapter preserves that safer
behavior; use parameterized queries instead of enabling the legacy method.
diff --git a/esapi/pom.xml b/esapi/pom.xml
index 859e767..6f6cf75 100644
--- a/esapi/pom.xml
+++ b/esapi/pom.xml
@@ -72,6 +72,28 @@
+
+
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ 5.6.4
+
+
+ org.apache.httpcomponents.core5
+ httpcore5
+ 5.4.4
+
+
+ org.apache.httpcomponents.core5
+ httpcore5-h2
+ 5.4.4
+
+
+
+
org.owasp.encoder
@@ -83,6 +105,27 @@
esapi${esapi.version}
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.apache.httpcomponents.core5
+ httpcore5
+
+
+ org.apache.httpcomponents.core5
+ httpcore5-h2
+ org.jsoup
diff --git a/esapi/src/test/java/org/owasp/encoder/esapi/ESAPIContextTest.java b/esapi/src/test/java/org/owasp/encoder/esapi/ESAPIContextTest.java
index 5b5e1eb..e1ca466 100644
--- a/esapi/src/test/java/org/owasp/encoder/esapi/ESAPIContextTest.java
+++ b/esapi/src/test/java/org/owasp/encoder/esapi/ESAPIContextTest.java
@@ -178,6 +178,25 @@ public void testJavaScriptTemplateBoundaryAndUtf8ValuePreservation() {
String encoded = encoder.encodeForJavaScript(input);
assertEquals("\\x24\\x7bvalue}\\x60\\x7f\\x85\\ud800|\\udfff|\ud83d\ude00", encoded);
assertEquals(encoded, new String(encoded.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8));
- assertEquals("\\x7bexecuted=true}", encoder.encodeForJavaScript("{executed=true}"));
+ assertEquals("\\x7bexe\\x63u\\x74ed=\\x74\\x72ue}",
+ encoder.encodeForJavaScript("{executed=true}"));
+ }
+
+ public void testJavaScriptCannotCloseHtmlScriptAcrossAdapterBoundary() {
+ String token = "";
+ for (int start = 0; start < token.length(); start++) {
+ for (int end = start + 1; end <= token.length(); end++) {
+ String html = "";
+ Document parsed = Jsoup.parse(html);
+ assertEquals(start + ":" + end, 0, parsed.select("img#pwn").size());
+ assertEquals(start + ":" + end, 1, parsed.select("script").size());
+ }
+ }
+
+ Document raw = Jsoup.parse("");
+ assertEquals("raw boundary negative control", 1, raw.select("img#pwn").size());
}
}
diff --git a/jakarta/pom.xml b/jakarta/pom.xml
index e9ad568..fd146cf 100644
--- a/jakarta/pom.xml
+++ b/jakarta/pom.xml
@@ -95,6 +95,13 @@
6.0.0test
+
+
+ org.jsoup
+ jsoup
+ 1.23.2
+ test
+
diff --git a/jakarta/src/main/resources/META-INF/java-encoder-advanced.tld b/jakarta/src/main/resources/META-INF/java-encoder-advanced.tld
index b40b586..a407038 100644
--- a/jakarta/src/main/resources/META-INF/java-encoder-advanced.tld
+++ b/jakarta/src/main/resources/META-INF/java-encoder-advanced.tld
@@ -6,10 +6,12 @@
owasp.encoder.jakarta.advanced
- Encodes data for an XML CDATA section. Replaces a terminating "]]>" with
- "]]]]><![CDATA[>". Invalid XML 1.0 characters, U+007F, C1 controls other than
- U+0085 (NEL), and Unicode noncharacters are replaced by a space. XML parsers normalize
- line endings. The caller must supply the CDATA section boundaries.
+ Encodes data for an XML CDATA section. Represents every "]" and ">" with
+ close/reopen sequences, preserving parsed character data and preventing encoded text
+ from completing a terminator supplied partly by adjacent trusted text. Invalid XML 1.0
+ characters, U+007F, C1 controls other than U+0085 (NEL), and Unicode noncharacters are
+ replaced by a space. XML parsers normalize line endings. The caller must supply the
+ CDATA section boundaries.
forCDATAforCDATA
@@ -111,7 +113,9 @@
Encodes a JavaScript string for HTML event attributes (such as onclick), HTML script
- blocks, or standalone JavaScript source.
+ blocks, or standalone JavaScript source. In HTML script blocks, additional hex
+ escapes prevent encoded fragments from completing </script, <!--, or -->
+ tokens across trusted boundaries; hyphen uses \x2d.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
@@ -139,8 +143,9 @@
Encodes a JavaScript string in an HTML event attribute (such as onclick). Not safe in
- HTML script blocks. Uses the same encoding as forJavaScript except that slash and
- hyphen are not escaped.
+ HTML script blocks. Uses the same encoding as forJavaScript except that slash,
+ hyphen, space, !, <, >, and both cases of C, I, P, R, S, and T are not
+ escaped because these characters protect the outer HTML script context.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
@@ -197,8 +202,9 @@
Encodes a JavaScript string in a standalone JavaScript file. Not safe in any context
- embedded in HTML. Uses the same encoding as forJavaScript except that slash, hyphen,
- and ampersand are not escaped and double and single quotes use backslash escapes.
+ embedded in HTML. Uses the same encoding as forJavaScript except that slash,
+ hyphen, space, !, <, >, both cases of C, I, P, R, S, and T, and ampersand
+ are not escaped; double and single quotes use backslash escapes.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
@@ -318,7 +324,8 @@
- Encoder for XML comments. NOT FOR USE WITH (X)HTML CONTEXTS.
+ Encoder for XML comments. Every input hyphen is replaced with a tilde.
+ NOT FOR USE WITH (X)HTML CONTEXTS.
(X)HTML comments may be interpreted by browsers as something
other than a comment, typically in vendor specific extensions
(e.g. <--if[IE]-->.
@@ -612,7 +619,8 @@
- Encoder for XML comments. NOT FOR USE WITH (X)HTML CONTEXTS.
+ Encoder for XML comments. Every input hyphen is replaced with a tilde.
+ NOT FOR USE WITH (X)HTML CONTEXTS.
(X)HTML comments may be interpreted by browsers as something
other than a comment, typically in vendor specific extensions
(e.g. <--if[IE]-->.
@@ -627,10 +635,12 @@
- Encodes data for an XML CDATA section. Replaces a terminating "]]>" with
- "]]]]><![CDATA[>". Invalid XML 1.0 characters, U+007F, C1 controls other than
- U+0085 (NEL), and Unicode noncharacters are replaced by a space. XML parsers normalize
- line endings. The caller must supply the CDATA section boundaries.
+ Encodes data for an XML CDATA section. Represents every "]" and ">" with
+ close/reopen sequences, preserving parsed character data and preventing encoded text
+ from completing a terminator supplied partly by adjacent trusted text. Invalid XML 1.0
+ characters, U+007F, C1 controls other than U+0085 (NEL), and Unicode noncharacters are
+ replaced by a space. XML parsers normalize line endings. The caller must supply the
+ CDATA section boundaries.
forCDATAforCDATA
@@ -641,7 +651,9 @@
Encodes a JavaScript string for HTML event attributes (such as onclick), HTML script
- blocks, or standalone JavaScript source.
+ blocks, or standalone JavaScript source. In HTML script blocks, additional hex
+ escapes prevent encoded fragments from completing </script, <!--, or -->
+ tokens across trusted boundaries; hyphen uses \x2d.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
@@ -663,8 +675,9 @@
Encodes a JavaScript string in an HTML event attribute (such as onclick). Not safe in
- HTML script blocks. Uses the same encoding as forJavaScript except that slash and
- hyphen are not escaped.
+ HTML script blocks. Uses the same encoding as forJavaScript except that slash,
+ hyphen, space, !, <, >, and both cases of C, I, P, R, S, and T are not
+ escaped because these characters protect the outer HTML script context.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
@@ -709,8 +722,9 @@
Encodes a JavaScript string in a standalone JavaScript file. Not safe in any context
- embedded in HTML. Uses the same encoding as forJavaScript except that slash, hyphen,
- and ampersand are not escaped and double and single quotes use backslash escapes.
+ embedded in HTML. Uses the same encoding as forJavaScript except that slash,
+ hyphen, space, !, <, >, both cases of C, I, P, R, S, and T, and ampersand
+ are not escaped; double and single quotes use backslash escapes.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
diff --git a/jakarta/src/main/resources/META-INF/java-encoder.tld b/jakarta/src/main/resources/META-INF/java-encoder.tld
index 621bfbd..5f8d32b 100644
--- a/jakarta/src/main/resources/META-INF/java-encoder.tld
+++ b/jakarta/src/main/resources/META-INF/java-encoder.tld
@@ -9,10 +9,12 @@
owasp.encoder.jakarta
- Encodes data for an XML CDATA section. Replaces a terminating "]]>" with
- "]]]]><![CDATA[>". Invalid XML 1.0 characters, U+007F, C1 controls other than
- U+0085 (NEL), and Unicode noncharacters are replaced by a space. XML parsers normalize
- line endings. The caller must supply the CDATA section boundaries.
+ Encodes data for an XML CDATA section. Represents every "]" and ">" with
+ close/reopen sequences, preserving parsed character data and preventing encoded text
+ from completing a terminator supplied partly by adjacent trusted text. Invalid XML 1.0
+ characters, U+007F, C1 controls other than U+0085 (NEL), and Unicode noncharacters are
+ replaced by a space. XML parsers normalize line endings. The caller must supply the
+ CDATA section boundaries.
forCDATAforCDATA
@@ -97,7 +99,9 @@
Encodes a JavaScript string for HTML event attributes (such as onclick), HTML script
- blocks, or standalone JavaScript source.
+ blocks, or standalone JavaScript source. In HTML script blocks, additional hex
+ escapes prevent encoded fragments from completing </script, <!--, or -->
+ tokens across trusted boundaries; hyphen uses \x2d.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
@@ -449,10 +453,12 @@
- Encodes data for an XML CDATA section. Replaces a terminating "]]>" with
- "]]]]><![CDATA[>". Invalid XML 1.0 characters, U+007F, C1 controls other than
- U+0085 (NEL), and Unicode noncharacters are replaced by a space. XML parsers normalize
- line endings. The caller must supply the CDATA section boundaries.
+ Encodes data for an XML CDATA section. Represents every "]" and ">" with
+ close/reopen sequences, preserving parsed character data and preventing encoded text
+ from completing a terminator supplied partly by adjacent trusted text. Invalid XML 1.0
+ characters, U+007F, C1 controls other than U+0085 (NEL), and Unicode noncharacters are
+ replaced by a space. XML parsers normalize line endings. The caller must supply the
+ CDATA section boundaries.
forCDATAforCDATA
@@ -463,7 +469,9 @@
Encodes a JavaScript string for HTML event attributes (such as onclick), HTML script
- blocks, or standalone JavaScript source.
+ blocks, or standalone JavaScript source. In HTML script blocks, additional hex
+ escapes prevent encoded fragments from completing </script, <!--, or -->
+ tokens across trusted boundaries; hyphen uses \x2d.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
diff --git a/jakarta/src/test/java/org/owasp/encoder/tag/TagEncodingTest.java b/jakarta/src/test/java/org/owasp/encoder/tag/TagEncodingTest.java
index d8b8bdf..64cb7f0 100644
--- a/jakarta/src/test/java/org/owasp/encoder/tag/TagEncodingTest.java
+++ b/jakarta/src/test/java/org/owasp/encoder/tag/TagEncodingTest.java
@@ -40,9 +40,13 @@
import junit.framework.Test;
import junit.framework.TestSuite;
import org.owasp.encoder.Encode;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
/** A named JUnit 3 parameterized case for every advanced tag and input. */
public class TagEncodingTest extends EncodingTagTest {
+ private static final String SCRIPT_BOUNDARY_INPUT = "script>";
+
private final Class extends EncodingTag> tagClass;
private final Method facade;
private final String input;
@@ -64,6 +68,10 @@ public static Test suite() throws Exception {
add(suite, type, method, "null", null);
add(suite, type, method, "empty", "");
add(suite, type, method, "plain", "plain text");
+ if ("forJavaScript".equals(method.getName())
+ || "forJavaScriptBlock".equals(method.getName())) {
+ add(suite, type, method, "script-boundary", SCRIPT_BOUNDARY_INPUT);
+ }
String hostile = "\"'<>&/\\`$={}() :;?#%+--]]>\0\t\r\n\u007f\u0085\u2028\u2029"
+ "\u00e9\ud83d\ude00\ud800X\udc00\uffff\ud800";
add(suite, type, method, "hostile-unicode", hostile);
@@ -91,6 +99,23 @@ protected void runTest() throws Exception {
tag.setJspContext(_pageContext);
tag.setValue(input);
tag.doTag();
- assertEquals(getName(), expected, _response.getContentAsString());
+ String actual = _response.getContentAsString();
+ assertEquals(getName(), expected, actual);
+ if (SCRIPT_BOUNDARY_INPUT.equals(input)) {
+ assertScriptContained("TLD function target", expected);
+ assertScriptContained("tag", actual);
+ }
+ }
+
+ private void assertScriptContained(String path, String encoded) {
+ Document parsed = Jsoup.parse("
after
");
+ assertEquals(path + " script", 1, parsed.select("script").size());
+ assertEquals(path + " injection", 0, parsed.select("img#pwn").size());
+ assertEquals(path + " trailing document", 1, parsed.select("p#after").size());
+
+ Document raw = Jsoup.parse("");
+ assertEquals(path + " negative control", 1, raw.select("img#pwn").size());
}
}
diff --git a/jsp/pom.xml b/jsp/pom.xml
index d4b3054..a426750 100644
--- a/jsp/pom.xml
+++ b/jsp/pom.xml
@@ -95,6 +95,13 @@
3.0.1test
+
+
+ org.jsoup
+ jsoup
+ 1.23.2
+ test
+
diff --git a/jsp/src/main/resources/META-INF/java-encoder-advanced.tld b/jsp/src/main/resources/META-INF/java-encoder-advanced.tld
index 6ec5ed3..355abc9 100644
--- a/jsp/src/main/resources/META-INF/java-encoder-advanced.tld
+++ b/jsp/src/main/resources/META-INF/java-encoder-advanced.tld
@@ -6,10 +6,12 @@
https://www.owasp.org/index.php/OWASP_Java_Encoder_Project#advanced
- Encodes data for an XML CDATA section. Replaces a terminating "]]>" with
- "]]]]><![CDATA[>". Invalid XML 1.0 characters, U+007F, C1 controls other than
- U+0085 (NEL), and Unicode noncharacters are replaced by a space. XML parsers normalize
- line endings. The caller must supply the CDATA section boundaries.
+ Encodes data for an XML CDATA section. Represents every "]" and ">" with
+ close/reopen sequences, preserving parsed character data and preventing encoded text
+ from completing a terminator supplied partly by adjacent trusted text. Invalid XML 1.0
+ characters, U+007F, C1 controls other than U+0085 (NEL), and Unicode noncharacters are
+ replaced by a space. XML parsers normalize line endings. The caller must supply the
+ CDATA section boundaries.
forCDATAforCDATA
@@ -111,7 +113,9 @@
Encodes a JavaScript string for HTML event attributes (such as onclick), HTML script
- blocks, or standalone JavaScript source.
+ blocks, or standalone JavaScript source. In HTML script blocks, additional hex
+ escapes prevent encoded fragments from completing </script, <!--, or -->
+ tokens across trusted boundaries; hyphen uses \x2d.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
@@ -139,8 +143,9 @@
Encodes a JavaScript string in an HTML event attribute (such as onclick). Not safe in
- HTML script blocks. Uses the same encoding as forJavaScript except that slash and
- hyphen are not escaped.
+ HTML script blocks. Uses the same encoding as forJavaScript except that slash,
+ hyphen, space, !, <, >, and both cases of C, I, P, R, S, and T are not
+ escaped because these characters protect the outer HTML script context.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
@@ -197,8 +202,9 @@
Encodes a JavaScript string in a standalone JavaScript file. Not safe in any context
- embedded in HTML. Uses the same encoding as forJavaScript except that slash, hyphen,
- and ampersand are not escaped and double and single quotes use backslash escapes.
+ embedded in HTML. Uses the same encoding as forJavaScript except that slash,
+ hyphen, space, !, <, >, both cases of C, I, P, R, S, and T, and ampersand
+ are not escaped; double and single quotes use backslash escapes.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
@@ -318,7 +324,8 @@
- Encoder for XML comments. NOT FOR USE WITH (X)HTML CONTEXTS.
+ Encoder for XML comments. Every input hyphen is replaced with a tilde.
+ NOT FOR USE WITH (X)HTML CONTEXTS.
(X)HTML comments may be interpreted by browsers as something
other than a comment, typically in vendor specific extensions
(e.g. <--if[IE]-->.
@@ -612,7 +619,8 @@
- Encoder for XML comments. NOT FOR USE WITH (X)HTML CONTEXTS.
+ Encoder for XML comments. Every input hyphen is replaced with a tilde.
+ NOT FOR USE WITH (X)HTML CONTEXTS.
(X)HTML comments may be interpreted by browsers as something
other than a comment, typically in vendor specific extensions
(e.g. <--if[IE]-->.
@@ -627,10 +635,12 @@
- Encodes data for an XML CDATA section. Replaces a terminating "]]>" with
- "]]]]><![CDATA[>". Invalid XML 1.0 characters, U+007F, C1 controls other than
- U+0085 (NEL), and Unicode noncharacters are replaced by a space. XML parsers normalize
- line endings. The caller must supply the CDATA section boundaries.
+ Encodes data for an XML CDATA section. Represents every "]" and ">" with
+ close/reopen sequences, preserving parsed character data and preventing encoded text
+ from completing a terminator supplied partly by adjacent trusted text. Invalid XML 1.0
+ characters, U+007F, C1 controls other than U+0085 (NEL), and Unicode noncharacters are
+ replaced by a space. XML parsers normalize line endings. The caller must supply the
+ CDATA section boundaries.
forCDATAforCDATA
@@ -641,7 +651,9 @@
Encodes a JavaScript string for HTML event attributes (such as onclick), HTML script
- blocks, or standalone JavaScript source.
+ blocks, or standalone JavaScript source. In HTML script blocks, additional hex
+ escapes prevent encoded fragments from completing </script, <!--, or -->
+ tokens across trusted boundaries; hyphen uses \x2d.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
@@ -663,8 +675,9 @@
Encodes a JavaScript string in an HTML event attribute (such as onclick). Not safe in
- HTML script blocks. Uses the same encoding as forJavaScript except that slash and
- hyphen are not escaped.
+ HTML script blocks. Uses the same encoding as forJavaScript except that slash,
+ hyphen, space, !, <, >, and both cases of C, I, P, R, S, and T are not
+ escaped because these characters protect the outer HTML script context.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
@@ -709,8 +722,9 @@
Encodes a JavaScript string in a standalone JavaScript file. Not safe in any context
- embedded in HTML. Uses the same encoding as forJavaScript except that slash, hyphen,
- and ampersand are not escaped and double and single quotes use backslash escapes.
+ embedded in HTML. Uses the same encoding as forJavaScript except that slash,
+ hyphen, space, !, <, >, both cases of C, I, P, R, S, and T, and ampersand
+ are not escaped; double and single quotes use backslash escapes.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
diff --git a/jsp/src/main/resources/META-INF/java-encoder.tld b/jsp/src/main/resources/META-INF/java-encoder.tld
index a676f0c..8b850df 100644
--- a/jsp/src/main/resources/META-INF/java-encoder.tld
+++ b/jsp/src/main/resources/META-INF/java-encoder.tld
@@ -6,10 +6,12 @@
https://www.owasp.org/index.php/OWASP_Java_Encoder_Project
- Encodes data for an XML CDATA section. Replaces a terminating "]]>" with
- "]]]]><![CDATA[>". Invalid XML 1.0 characters, U+007F, C1 controls other than
- U+0085 (NEL), and Unicode noncharacters are replaced by a space. XML parsers normalize
- line endings. The caller must supply the CDATA section boundaries.
+ Encodes data for an XML CDATA section. Represents every "]" and ">" with
+ close/reopen sequences, preserving parsed character data and preventing encoded text
+ from completing a terminator supplied partly by adjacent trusted text. Invalid XML 1.0
+ characters, U+007F, C1 controls other than U+0085 (NEL), and Unicode noncharacters are
+ replaced by a space. XML parsers normalize line endings. The caller must supply the
+ CDATA section boundaries.
forCDATAforCDATA
@@ -94,7 +96,9 @@
Encodes a JavaScript string for HTML event attributes (such as onclick), HTML script
- blocks, or standalone JavaScript source.
+ blocks, or standalone JavaScript source. In HTML script blocks, additional hex
+ escapes prevent encoded fragments from completing </script, <!--, or -->
+ tokens across trusted boundaries; hyphen uses \x2d.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
@@ -446,10 +450,12 @@
- Encodes data for an XML CDATA section. Replaces a terminating "]]>" with
- "]]]]><![CDATA[>". Invalid XML 1.0 characters, U+007F, C1 controls other than
- U+0085 (NEL), and Unicode noncharacters are replaced by a space. XML parsers normalize
- line endings. The caller must supply the CDATA section boundaries.
+ Encodes data for an XML CDATA section. Represents every "]" and ">" with
+ close/reopen sequences, preserving parsed character data and preventing encoded text
+ from completing a terminator supplied partly by adjacent trusted text. Invalid XML 1.0
+ characters, U+007F, C1 controls other than U+0085 (NEL), and Unicode noncharacters are
+ replaced by a space. XML parsers normalize line endings. The caller must supply the
+ CDATA section boundaries.
forCDATAforCDATA
@@ -460,7 +466,9 @@
Encodes a JavaScript string for HTML event attributes (such as onclick), HTML script
- blocks, or standalone JavaScript source.
+ blocks, or standalone JavaScript source. In HTML script blocks, additional hex
+ escapes prevent encoded fragments from completing </script, <!--, or -->
+ tokens across trusted boundaries; hyphen uses \x2d.
Caller must supply single (') or double (") quotation marks, or backticks (`)
for an ordinary (untagged) template literal. Insert encoded output into literal text,
not a ${...} expression. Dollar sign ($), backtick, and opening brace ({) are encoded
diff --git a/jsp/src/test/java/org/owasp/encoder/tag/TagEncodingTest.java b/jsp/src/test/java/org/owasp/encoder/tag/TagEncodingTest.java
index d8b8bdf..64cb7f0 100644
--- a/jsp/src/test/java/org/owasp/encoder/tag/TagEncodingTest.java
+++ b/jsp/src/test/java/org/owasp/encoder/tag/TagEncodingTest.java
@@ -40,9 +40,13 @@
import junit.framework.Test;
import junit.framework.TestSuite;
import org.owasp.encoder.Encode;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
/** A named JUnit 3 parameterized case for every advanced tag and input. */
public class TagEncodingTest extends EncodingTagTest {
+ private static final String SCRIPT_BOUNDARY_INPUT = "script>";
+
private final Class extends EncodingTag> tagClass;
private final Method facade;
private final String input;
@@ -64,6 +68,10 @@ public static Test suite() throws Exception {
add(suite, type, method, "null", null);
add(suite, type, method, "empty", "");
add(suite, type, method, "plain", "plain text");
+ if ("forJavaScript".equals(method.getName())
+ || "forJavaScriptBlock".equals(method.getName())) {
+ add(suite, type, method, "script-boundary", SCRIPT_BOUNDARY_INPUT);
+ }
String hostile = "\"'<>&/\\`$={}() :;?#%+--]]>\0\t\r\n\u007f\u0085\u2028\u2029"
+ "\u00e9\ud83d\ude00\ud800X\udc00\uffff\ud800";
add(suite, type, method, "hostile-unicode", hostile);
@@ -91,6 +99,23 @@ protected void runTest() throws Exception {
tag.setJspContext(_pageContext);
tag.setValue(input);
tag.doTag();
- assertEquals(getName(), expected, _response.getContentAsString());
+ String actual = _response.getContentAsString();
+ assertEquals(getName(), expected, actual);
+ if (SCRIPT_BOUNDARY_INPUT.equals(input)) {
+ assertScriptContained("TLD function target", expected);
+ assertScriptContained("tag", actual);
+ }
+ }
+
+ private void assertScriptContained(String path, String encoded) {
+ Document parsed = Jsoup.parse("
after
");
+ assertEquals(path + " script", 1, parsed.select("script").size());
+ assertEquals(path + " injection", 0, parsed.select("img#pwn").size());
+ assertEquals(path + " trailing document", 1, parsed.select("p#after").size());
+
+ Document raw = Jsoup.parse("");
+ assertEquals(path + " negative control", 1, raw.select("img#pwn").size());
}
}
diff --git a/pom.xml b/pom.xml
index 0a11004..6bf2c34 100644
--- a/pom.xml
+++ b/pom.xml
@@ -73,7 +73,9 @@
-
+ scm:git:git@github.com:OWASP/owasp-java-encoder.gitscm:git:https://github.com/OWASP/owasp-java-encoder.githttps://github.com/OWASP/owasp-java-encoder
@@ -117,6 +119,9 @@
2026-09-26T00:00:00ZUTF-8UTF-8
+
+ 1.4.1
@@ -353,7 +358,7 @@
-
+
com.github.siom79.japicmpjapicmp-maven-plugin
@@ -363,7 +368,7 @@
${project.groupId}${project.artifactId}
- 1.4.0
+ ${public.api.baseline.version}jar
diff --git a/scripts/check-effective-pom-scm.py b/scripts/check-effective-pom-scm.py
new file mode 100644
index 0000000..0fbd7be
--- /dev/null
+++ b/scripts/check-effective-pom-scm.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+"""Verify published modules' actual effective SCM metadata against the parent."""
+
+import argparse
+import os
+from pathlib import Path
+import subprocess
+import tempfile
+import xml.etree.ElementTree as ET
+
+
+ROOT = Path(__file__).resolve().parents[1]
+NAMESPACE = 'http://maven.apache.org/POM/4.0.0'
+FIELDS = ('connection', 'developerConnection', 'url')
+MODULES = {'': 'encoder-parent', 'core': 'encoder', 'jsp': 'encoder-jsp',
+ 'jakarta': 'encoder-jakarta-jsp', 'esapi': 'encoder-esapi'}
+MODULE_SUFFIXES = tuple('/' + name for name in
+ ('core', 'jsp', 'jakarta', 'esapi', 'encoder',
+ 'encoder-jsp', 'encoder-jakarta-jsp', 'encoder-esapi'))
+
+
+def qualified(name):
+ return '{' + NAMESPACE + '}' + name
+
+
+def read_scm(path, artifact):
+ """Read SCM fields from a Maven effective POM, rejecting absent metadata."""
+ project = ET.parse(path).getroot()
+ if project.tag != qualified('project'):
+ raise ValueError(str(path) + ': expected a Maven effective POM')
+ actual_artifact = project.findtext(qualified('artifactId'))
+ if actual_artifact != artifact:
+ raise ValueError(str(path) + ': expected artifactId ' + artifact +
+ ', found ' + repr(actual_artifact))
+ scm = project.find(qualified('scm'))
+ if scm is None:
+ raise ValueError(artifact + ': missing effective SCM metadata')
+ values = {}
+ for field in FIELDS:
+ element = scm.find(qualified(field))
+ value = element.text.strip() if element is not None and element.text else ''
+ if not value:
+ raise ValueError(artifact + ': missing effective scm/' + field)
+ values[field] = value
+ return values
+
+
+def check_effective_poms(paths):
+ """Compare the five parsed effective POMs, including inherited SCM paths."""
+ if set(paths) != set(MODULES):
+ raise ValueError('Expected effective POMs for parent, core, jsp, jakarta and esapi')
+ parent = read_scm(paths[''], MODULES[''])
+ for field, value in parent.items():
+ if value.rstrip('/').endswith(MODULE_SUFFIXES):
+ raise ValueError('encoder-parent: scm/' + field +
+ ' contains a module-appended path: ' + value)
+ for module, artifact in MODULES.items():
+ if not module:
+ continue
+ child = read_scm(paths[module], artifact)
+ for field in FIELDS:
+ if child[field] != parent[field]:
+ suffix = (' (module-appended path)' if child[field].startswith(
+ parent[field].rstrip('/') + '/') else '')
+ raise ValueError(artifact + ': effective scm/' + field +
+ ' differs from encoder-parent' + suffix + ': ' +
+ repr(child[field]) + ' != ' + repr(parent[field]))
+
+
+def generate_and_check(root, maven, repository=None, settings=None, options=()):
+ """Run the committed wrapper outside the Maven lifecycle for each published POM."""
+ with tempfile.TemporaryDirectory(prefix='encoder-effective-scm-') as directory:
+ paths = {}
+ for module, artifact in MODULES.items():
+ output = Path(directory) / (artifact + '.xml')
+ pom = root / module / 'pom.xml'
+ command = [str(maven), '-B', '-ntp', '-N', '-f', str(pom)]
+ if repository is not None:
+ command.append('-Dmaven.repo.local=' + str(repository))
+ if settings is not None:
+ command.extend(('-s', str(settings)))
+ command.extend(options)
+ command.extend(('help:effective-pom', '-Doutput=' + str(output)))
+ subprocess.run(command, cwd=root, check=True)
+ if not output.is_file():
+ raise ValueError(artifact + ': Maven did not write an effective POM')
+ paths[module] = output
+ check_effective_poms(paths)
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--root', type=Path, default=ROOT)
+ parser.add_argument('--maven', type=Path,
+ help='Maven executable (default: the committed wrapper)')
+ parser.add_argument('--repository', type=Path,
+ help='Maven local repository (-Dmaven.repo.local)')
+ parser.add_argument('--settings', type=Path, help='Maven settings.xml')
+ parser.add_argument('--maven-option', action='append', default=[],
+ help='Additional Maven argument; repeat, e.g. --maven-option=-o')
+ args = parser.parse_args()
+ root = args.root.resolve()
+ maven = args.maven or root / ('mvnw.cmd' if os.name == 'nt' else 'mvnw')
+ repository = args.repository.resolve() if args.repository else None
+ settings = args.settings.resolve() if args.settings else None
+ if settings is not None and not settings.is_file():
+ parser.error('Maven settings file does not exist: ' + str(settings))
+ try:
+ generate_and_check(root, maven, repository, settings, args.maven_option)
+ except (ValueError, ET.ParseError, subprocess.CalledProcessError) as error:
+ parser.exit(1, str(error) + '\n')
+ print('Effective SCM metadata matches encoder-parent for all four published modules')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/tests/test_release_baselines.py b/scripts/tests/test_release_baselines.py
new file mode 100644
index 0000000..391a5ac
--- /dev/null
+++ b/scripts/tests/test_release_baselines.py
@@ -0,0 +1,76 @@
+"""Release guards derived from immutable semantic-version tags."""
+
+from pathlib import Path
+import re
+import subprocess
+import unittest
+import xml.etree.ElementTree as ET
+
+
+ROOT = Path(__file__).resolve().parents[2]
+NS = {'p': 'http://maven.apache.org/POM/4.0.0'}
+RELEASE_TAG = re.compile(r'^v(\d+)\.(\d+)\.(\d+)$')
+PROJECT_VERSION = re.compile(r'^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$')
+
+
+def immutable_release_versions():
+ versions = []
+ tags = subprocess.check_output(
+ ['git', 'tag', '--merged', 'HEAD', '--list', 'v*'], cwd=ROOT,
+ text=True).splitlines()
+ for tag in tags:
+ match = RELEASE_TAG.match(tag)
+ if match:
+ versions.append((tuple(int(part) for part in match.groups()), tag[1:]))
+ if not versions:
+ raise ValueError('No immutable semantic-version release tags found')
+ return versions
+
+
+def version_tuple(version):
+ match = PROJECT_VERSION.match(version)
+ if not match:
+ raise ValueError('Project version is not semantic: ' + version)
+ return tuple(int(part) for part in match.groups())
+
+
+def preceding_release(versions, project_version):
+ """Return the newest immutable release strictly older than the build."""
+ current = version_tuple(project_version)
+ preceding = [release for release in versions if release[0] < current]
+ if not preceding:
+ raise ValueError('No immutable release precedes ' + project_version)
+ return max(preceding)[1]
+
+
+class BaselineSelection(unittest.TestCase):
+ def test_release_tag_at_head_is_not_its_own_baseline(self):
+ releases = [((1, 4, 1), '1.4.1'), ((1, 5, 0), '1.5.0')]
+ self.assertEqual('1.4.1', preceding_release(releases, '1.5.0'))
+
+ def test_next_snapshot_uses_the_completed_release(self):
+ releases = [((1, 4, 1), '1.4.1'), ((1, 5, 0), '1.5.0')]
+ self.assertEqual('1.5.0', preceding_release(releases, '1.5.1-SNAPSHOT'))
+
+
+class PublicApiBaseline(unittest.TestCase):
+ def test_japicmp_uses_preceding_immutable_release(self):
+ pom = ET.parse(ROOT / 'pom.xml').getroot()
+ project_version = pom.findtext('p:version', namespaces=NS)
+ expected = preceding_release(immutable_release_versions(), project_version)
+ baseline = pom.findtext('p:properties/p:public.api.baseline.version',
+ namespaces=NS)
+ self.assertEqual(expected, baseline)
+
+ plugins = pom.findall('p:build/p:plugins/p:plugin', NS)
+ japicmp = next(plugin for plugin in plugins
+ if plugin.findtext('p:artifactId', namespaces=NS)
+ == 'japicmp-maven-plugin')
+ configured = japicmp.findtext(
+ 'p:configuration/p:oldVersion/p:dependency/p:version',
+ namespaces=NS)
+ self.assertEqual('${public.api.baseline.version}', configured)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/scripts/tests/test_scm_metadata.py b/scripts/tests/test_scm_metadata.py
new file mode 100644
index 0000000..0f2e476
--- /dev/null
+++ b/scripts/tests/test_scm_metadata.py
@@ -0,0 +1,84 @@
+"""Fixtures for the effective POM SCM inheritance guard."""
+
+import importlib.util
+from pathlib import Path
+import tempfile
+import unittest
+import xml.etree.ElementTree as ET
+
+
+ROOT = Path(__file__).resolve().parents[2]
+spec = importlib.util.spec_from_file_location(
+ 'check_effective_pom_scm', ROOT / 'scripts/check-effective-pom-scm.py')
+guard = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(guard)
+
+BASE_SCM = {
+ 'connection': 'scm:git:https://github.com/OWASP/owasp-java-encoder.git',
+ 'developerConnection': 'scm:git:git@github.com:OWASP/owasp-java-encoder.git',
+ 'url': 'https://github.com/OWASP/owasp-java-encoder',
+}
+
+
+class EffectiveScmFixtures(unittest.TestCase):
+ def setUp(self):
+ self.temp = tempfile.TemporaryDirectory()
+ self.addCleanup(self.temp.cleanup)
+ self.paths = {}
+ for module, artifact in guard.MODULES.items():
+ self.write_fixture(module, artifact, BASE_SCM)
+
+ def write_fixture(self, module, artifact, scm):
+ project = ET.Element(guard.qualified('project'))
+ ET.SubElement(project, guard.qualified('artifactId')).text = artifact
+ metadata = ET.SubElement(project, guard.qualified('scm'))
+ for field, value in scm.items():
+ ET.SubElement(metadata, guard.qualified(field)).text = value
+ path = Path(self.temp.name) / (artifact + '.xml')
+ ET.ElementTree(project).write(path, encoding='utf-8', xml_declaration=True)
+ self.paths[module] = path
+
+ def test_matching_effective_metadata(self):
+ guard.check_effective_poms(self.paths)
+
+ def test_each_changed_field_is_rejected(self):
+ for field in guard.FIELDS:
+ with self.subTest(field=field):
+ altered = dict(BASE_SCM, **{field: BASE_SCM[field] + '-wrong'})
+ self.write_fixture('core', 'encoder', altered)
+ with self.assertRaisesRegex(ValueError, 'scm/' + field):
+ guard.check_effective_poms(self.paths)
+ self.write_fixture('core', 'encoder', BASE_SCM)
+
+ def test_each_module_appended_field_is_rejected(self):
+ for module, artifact in list(guard.MODULES.items())[1:]:
+ for field in guard.FIELDS:
+ with self.subTest(module=module, field=field):
+ altered = dict(BASE_SCM, **{field: BASE_SCM[field] + '/' + module})
+ self.write_fixture(module, artifact, altered)
+ with self.assertRaisesRegex(ValueError, 'module-appended path'):
+ guard.check_effective_poms(self.paths)
+ self.write_fixture(module, artifact, BASE_SCM)
+
+ def test_missing_field_and_wrong_artifact_are_rejected(self):
+ for field in guard.FIELDS:
+ with self.subTest(field=field):
+ self.write_fixture('jsp', 'encoder-jsp',
+ {key: value for key, value in BASE_SCM.items()
+ if key != field})
+ with self.assertRaisesRegex(ValueError, 'missing effective scm/' + field):
+ guard.check_effective_poms(self.paths)
+ self.write_fixture('jsp', 'encoder-jsp', BASE_SCM)
+ self.write_fixture('jsp', 'unrelated-artifact', BASE_SCM)
+ with self.assertRaisesRegex(ValueError, 'expected artifactId encoder-jsp'):
+ guard.check_effective_poms(self.paths)
+
+ def test_parent_module_path_is_rejected(self):
+ altered = dict(BASE_SCM, url=BASE_SCM['url'] + '/core')
+ self.write_fixture('', 'encoder-parent', altered)
+ with self.assertRaisesRegex(ValueError, 'module-appended path'):
+ guard.check_effective_poms(self.paths)
+
+
+if __name__ == '__main__':
+ unittest.main()
From 6441a23ad9e19f0ef5c18c490b9088d7255043b9 Mon Sep 17 00:00:00 2001
From: Jim Manico
Date: Sun, 27 Sep 2026 16:14:15 -0700
Subject: [PATCH 2/7] Retire ESAPI adapter and clean dependency graph
---
.github/CI_SECURITY.md | 44 ++-
.github/dependabot.yml | 1 -
.github/workflows/build.yaml | 40 +-
.github/workflows/consumer-compatibility.yaml | 13 +-
.github/workflows/dependency-submission.yaml | 18 +-
BUILDING.md | 14 +-
CHANGELOG.md | 18 +-
CONTRIBUTING.md | 9 +-
README.md | 33 +-
RELEASING.md | 26 +-
SECURITY.md | 6 +-
compatibility/README.md | 55 +--
compatibility/config/ESAPI.properties | 13 -
compatibility/config/validation.properties | 1 -
compatibility/consumers.py | 102 +----
compatibility/dependencies/esapi.xml | 36 --
compatibility/src/EsapiConsumer.java | 50 ---
compatibility/src/ModuleMetadata.java | 6 +-
compatibility/tests/test_guards.py | 28 +-
core/pom.xml | 13 +
docs/compatibility-decisions.md | 17 +-
docs/contexts.md | 9 +-
docs/dependencies.md | 85 ++---
docs/encoder-esapi-retirement.md | 54 +++
esapi/README.md | 229 ------------
esapi/pom.xml | 187 ----------
.../org/owasp/encoder/esapi/ESAPIEncoder.java | 353 ------------------
esapi/src/main/java9/module-info.java | 40 --
esapi/src/main/resources/META-INF/LICENSE | 33 --
esapi/src/site/site.xml | 41 --
.../owasp/encoder/esapi/ESAPIContextTest.java | 202 ----------
.../owasp/encoder/esapi/ESAPIEncoderTest.java | 191 ----------
.../esapi/ESAPIInitializationProbe.java | 105 ------
.../module-info.java | 37 --
.../owasp/encoder/consumer/EsapiConsumer.java | 55 ---
.../test/resources/.esapi/ESAPI.properties | 39 --
jakarta-test/pom.xml | 44 +++
jakarta/pom.xml | 13 +
jsp/pom.xml | 13 +
pom.xml | 117 ++++--
releases/1.4.1.md | 2 +-
releases/TEMPLATE.md | 3 +-
releases/batch-00-validation.md | 2 +-
scripts/build-dependency-snapshot.py | 39 +-
scripts/check-effective-pom-scm.py | 8 +-
scripts/check-reproducible.py | 10 +-
scripts/package-release.py | 6 +-
.../tests/test_build_dependency_snapshot.py | 24 ++
scripts/tests/test_ci_policy.py | 35 +-
scripts/tests/test_release_baselines.py | 32 +-
50 files changed, 518 insertions(+), 2033 deletions(-)
delete mode 100644 compatibility/config/ESAPI.properties
delete mode 100644 compatibility/config/validation.properties
delete mode 100644 compatibility/dependencies/esapi.xml
delete mode 100644 compatibility/src/EsapiConsumer.java
create mode 100644 docs/encoder-esapi-retirement.md
delete mode 100644 esapi/README.md
delete mode 100644 esapi/pom.xml
delete mode 100644 esapi/src/main/java/org/owasp/encoder/esapi/ESAPIEncoder.java
delete mode 100644 esapi/src/main/java9/module-info.java
delete mode 100644 esapi/src/main/resources/META-INF/LICENSE
delete mode 100644 esapi/src/site/site.xml
delete mode 100644 esapi/src/test/java/org/owasp/encoder/esapi/ESAPIContextTest.java
delete mode 100644 esapi/src/test/java/org/owasp/encoder/esapi/ESAPIEncoderTest.java
delete mode 100644 esapi/src/test/java/org/owasp/encoder/esapi/ESAPIInitializationProbe.java
delete mode 100644 esapi/src/test/modules/owasp.encoder.esapi.consumer/module-info.java
delete mode 100644 esapi/src/test/modules/owasp.encoder.esapi.consumer/org/owasp/encoder/consumer/EsapiConsumer.java
delete mode 100644 esapi/src/test/resources/.esapi/ESAPI.properties
diff --git a/.github/CI_SECURITY.md b/.github/CI_SECURITY.md
index 50670d0..455943e 100644
--- a/.github/CI_SECURITY.md
+++ b/.github/CI_SECURITY.md
@@ -4,10 +4,10 @@
`Java CI gate` requires the clean JDK 17 reactor build, JSP/Jakarta source parity,
CI policy tests, the Docker/Selenium browser test, exact reactor JAR inclusion in
-the Jakarta WAR, and every ESAPI version from 2.5.1.0 through 2.7.0.0.
+the Jakarta WAR.
`Packaged consumer gate` requires JDK 17 artifact preparation/API and Java 8
signature checks, package guard tests, the original packaged bytes on Java
-8/11/17/21/25, and core/JSP/ESAPI unit tests with the Java 8 JVM and coverage.
+8/11/17/21/25, and core/JSP unit tests with the Java 8 JVM and coverage.
JDK 21/25 build probes remain advisory. Require **both** gates: neither observes
the other workflow. The gates run with `always()` and accept only `success` for
every expected dependency; missing, failed, skipped and cancelled jobs fail.
@@ -34,15 +34,13 @@ See [local quarantine guidance](../RELEASING.md#maven-storage-and-repository-con
Baseline main runs [Java CI 36220505798](https://github.com/OWASP/owasp-java-encoder/actions/runs/36220505798)
and [consumers 36220505783](https://github.com/OWASP/owasp-java-encoder/actions/runs/36220505783)
had 14 Maven cache misses and one hit (Java 8 install job). Build took 3m14s;
-ESAPI jobs 65–97s; preparation 85s; Java 8 tests 87s; runtimes 12–16s.
-The separate clean test-compilation and install lifecycles are now one clean
-verify lifecycle. Further core sharing between ESAPI versions is deferred: it
-would complicate reactor resolution and package checks for little measured gain.
+preparation took 85s; Java 8 tests 87s; runtimes 12–16s. The separate clean
+test-compilation and install lifecycles are now one clean verify lifecycle.
## Scanning and dependency updates
Advanced CodeQL in `codeql.yaml` is the single analysis owner; leave default
-setup unconfigured. Java uses a manual JDK 17 build of all four libraries and
+setup unconfigured. Java uses a manual JDK 17 build of all three libraries and
the optional `testJakarta` application; Actions and Python tooling use extraction
without a build. It runs on PRs, main, a weekly schedule and manual dispatch.
Only analysis jobs request `security-events: write`; fork PRs use GitHub's
@@ -55,24 +53,32 @@ snapshot API. It builds its own checkout without caches or imported artifacts.
Separate correlators submit the normal reactor and the optional Jakarta profile.
The pinned Maven submission action includes all resolved project scopes,
including runtime, test and provided dependencies. Maven dependency plugin
-3.11.0 `resolve-plugins` separately resolves build/report plugins and their
-transitives; `scripts/build-dependency-snapshot.py` submits those edges as
-development dependencies. Graph reports and submission JSON are retained for
-inspection. Inspect representative ESAPI/AntiSamy HTTP transitives and Jakarta
-Spring/Tomcat dependencies in the resulting graph; alert counts are not gates.
-
-All four submissions use detector `encoder-maven-build-graph` with distinct,
-stable correlators. Keep the action's detector inputs synchronized with the
+3.11.0 `resolve-plugins` separately resolves the source-controlled allowlist of
+plugins actually invoked by verification, consumer installation, metadata checks,
+and release staging. The optional Jakarta app is resolved from its own smaller
+package-gate allowlist, so inherited but inactive Boot plugin-management entries
+are not submitted. The disabled Site plugin is likewise not represented as an
+executed dependency. `scripts/build-dependency-snapshot.py` submits those closures as
+development dependencies, once for shared root tooling and only the differing
+plugin closures for child POMs. Graph reports and submission JSON are retained
+for inspection. Inspect representative Jakarta Spring/Tomcat dependencies in the
+resulting graph; alert counts are not gates.
+
+All submissions use detector `encoder-maven-build-graph` with distinct, stable
+correlators. Keep the action's detector inputs synchronized with the
Python build snapshot: GitHub [merges correlators from the same detector](https://docs.github.com/en/code-security/concepts/supply-chain-security/dependency-graph-data#prioritization),
but selects between different detectors for a POM. Different detectors can hide
runtime dependencies behind build-only results despite successful submissions.
Check the final SBOM after both matrix jobs finish, including runtime versions
and development dependencies together, not just the snapshot API status.
-Dependabot checks all library POMs, the parent and optional app weekly, with
+Dependabot checks all current library POMs, the parent and optional app weekly, with
separate Maven and SHA-pinned Actions groups and grouped Maven security updates.
Normal review and complete CI apply to automated PRs; no automatic merging is
configured. Review new action source and transitive downloads as well as pins.
+Do not dismiss alerts merely to reduce the count. Correct versions or graph
+semantics, submit the new graph, and let GitHub close packages that are no longer
+present.
Baseline-sensitive API, JSP-engine and build-plugin dependencies are excluded only
from the broad Maven **version-update group**, so their proposals receive individual
review. They remain eligible for updates; the security-update group is unchanged.
@@ -85,9 +91,9 @@ an automatic baseline replacement. Inspect any advisory against its actual
local bundle-loading test scope, and record a specific disposition. Do not
suppress advisories across all Felix versions or application deployments.
-ESAPI advisory triage lives in [the adapter guide](../esapi/README.md#dependency-security-triage).
-Upstream fixes are preferred; tested mitigations remain possible. No transitive
-finding is dismissed merely because another library introduces it. Optional
+The retired `encoder-esapi` module is absent from Dependabot configuration and
+the submitted 1.5 dependency graph. Its historical artifacts are not rewritten;
+see the [retirement notice](../docs/encoder-esapi-retirement.md). Optional
Scorecard publication, best-practices registration and another scheduled scanner
are follow-ups, not prerequisites for these operating controls.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index ed10655..36258a2 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -6,7 +6,6 @@ updates:
- /core
- /jsp
- /jakarta
- - /esapi
- /jakarta-test
schedule:
interval: weekly
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 41d9cc7..dca13fe 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -73,51 +73,17 @@ jobs:
**/target/checkstyle-result.xml
jakarta-test/target/packaged-war.log
- esapi-compatibility:
- name: ESAPI ${{ matrix.esapi-version }}
- runs-on: ubuntu-latest
- timeout-minutes: 25
- strategy:
- fail-fast: false
- matrix:
- esapi-version:
- - '2.5.1.0'
- - '2.5.2.0'
- - '2.5.3.0'
- - '2.5.3.1'
- - '2.5.4.0'
- - '2.5.5.0'
- - '2.6.0.0'
- - '2.6.1.0'
- - '2.6.2.0'
- - '2.7.0.0'
- steps:
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- with:
- persist-credentials: false
- - name: Check versions and isolate Maven storage
- run: |
- python3 scripts/check-ci-version.py --ref "$GITHUB_REF"
- echo "MAVEN_OPTS=-Dmaven.repo.local=$RUNNER_TEMP/m2" >> "$GITHUB_ENV"
- - name: Set up JDK 17
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
- with:
- java-version: '17'
- distribution: 'temurin'
- - name: Test ESAPI compatibility
- run: ./mvnw -B -ntp -pl esapi -am verify -Desapi.version=${{ matrix.esapi-version }}
-
gate:
name: Java CI gate
if: ${{ always() }}
- needs: [build, esapi-compatibility]
+ needs: [build]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- - name: Require every build, browser and ESAPI matrix result
+ - name: Require the build and browser result
env:
NEEDS: ${{ toJSON(needs) }}
- run: python3 scripts/check-ci-gate.py build esapi-compatibility
+ run: python3 scripts/check-ci-gate.py build
diff --git a/.github/workflows/consumer-compatibility.yaml b/.github/workflows/consumer-compatibility.yaml
index 0e5f6fe..2dcea86 100644
--- a/.github/workflows/consumer-compatibility.yaml
+++ b/.github/workflows/consumer-compatibility.yaml
@@ -58,7 +58,6 @@ jobs:
**/target/surefire-reports/
**/target/failsafe-reports/
**/target/jsp-engine/
- **/target/japicmp/
**/target/site/jacoco/
runtime:
@@ -99,7 +98,7 @@ jobs:
path: runtime.log
java8-unit-tests:
- name: Core, JSP, and ESAPI unit tests on Java 8
+ name: Core and JSP unit tests on Java 8
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 20
@@ -118,12 +117,12 @@ jobs:
8
17
- name: Build with JDK 17
- run: ./mvnw -B -ntp -DskipTests install -pl core,jsp,esapi -am 2>&1 | tee build.log
+ run: ./mvnw -B -ntp -DskipTests install -pl core,jsp -am 2>&1 | tee build.log
- name: Run unit tests and collect coverage with Java 8
- run: ./mvnw -B -ntp -pl core,jsp,esapi jacoco:prepare-agent@prepare-agent surefire:test jacoco:report -Djvm="$JAVA_HOME_8_X64/bin/java" 2>&1 | tee java8-tests.log
+ run: ./mvnw -B -ntp -pl core,jsp jacoco:prepare-agent@prepare-agent surefire:test jacoco:report -Djvm="$JAVA_HOME_8_X64/bin/java" 2>&1 | tee java8-tests.log
- name: Confirm Java 8 execution and coverage in every tested module
run: |
- for module in core jsp esapi; do
+ for module in core jsp; do
grep -q 'name="java.specification.version" value="1.8"' "$module"/target/surefire-reports/TEST-*.xml || {
echo "::error::Missing Java 8 Surefire report for $module"
exit 1
@@ -143,13 +142,10 @@ jobs:
java8-tests.log
core/target/surefire-reports/
jsp/target/surefire-reports/
- esapi/target/surefire-reports/
core/target/site/jacoco/
jsp/target/site/jacoco/
- esapi/target/site/jacoco/
core/target/jacoco.exec
jsp/target/jacoco.exec
- esapi/target/jacoco.exec
forward-build:
name: Advisory build on JDK ${{ matrix.java }}
@@ -184,7 +180,6 @@ jobs:
**/target/surefire-reports/
**/target/failsafe-reports/
**/target/jsp-engine/
- **/target/japicmp/
**/target/site/jacoco/
wrapper:
diff --git a/.github/workflows/dependency-submission.yaml b/.github/workflows/dependency-submission.yaml
index b32c883..42064e5 100644
--- a/.github/workflows/dependency-submission.yaml
+++ b/.github/workflows/dependency-submission.yaml
@@ -29,8 +29,11 @@ jobs:
include:
- graph: libraries
profile: ''
+ plugins: maven-clean-plugin,maven-enforcer-plugin,maven-checkstyle-plugin,maven-compiler-plugin,maven-bundle-plugin,jacoco-maven-plugin,maven-surefire-plugin,maven-resources-plugin,maven-jar-plugin,build-helper-maven-plugin,maven-source-plugin,maven-javadoc-plugin,exec-maven-plugin,maven-failsafe-plugin,maven-antrun-plugin,maven-dependency-plugin,maven-help-plugin,animal-sniffer-maven-plugin,maven-install-plugin
- graph: jakarta-app
profile: -PtestJakarta
+ plugins: maven-clean-plugin,maven-enforcer-plugin,maven-checkstyle-plugin,maven-compiler-plugin,maven-bundle-plugin,jacoco-maven-plugin,maven-surefire-plugin,maven-resources-plugin,maven-jar-plugin,build-helper-maven-plugin,maven-source-plugin,maven-javadoc-plugin,exec-maven-plugin,maven-failsafe-plugin,maven-antrun-plugin,maven-dependency-plugin,maven-help-plugin,animal-sniffer-maven-plugin,maven-install-plugin
+ app_plugins: maven-clean-plugin,maven-enforcer-plugin,maven-checkstyle-plugin,maven-compiler-plugin,maven-surefire-plugin,maven-failsafe-plugin,maven-resources-plugin,maven-war-plugin,spring-boot-maven-plugin,maven-dependency-plugin
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
@@ -55,10 +58,15 @@ jobs:
detector-name: encoder-maven-build-graph
detector-version: '1.0.0'
detector-url: https://github.com/OWASP/owasp-java-encoder
- - name: Resolve build plugins and their dependencies
+ - name: Resolve shared library build plugins and their dependencies
env:
- PROFILE: ${{ matrix.profile }}
- run: ./mvnw -B -ntp ${PROFILE:+"$PROFILE"} org.apache.maven.plugins:maven-dependency-plugin:3.11.0:resolve-plugins -DoutputFile=target/build-dependencies.txt
+ PLUGINS: ${{ matrix.plugins }}
+ run: ./mvnw -B -ntp org.apache.maven.plugins:maven-dependency-plugin:3.11.0:resolve-plugins -DincludeArtifactIds="$PLUGINS" -DoutputFile=target/build-dependencies.txt
+ - name: Resolve only the optional app plugins invoked by its package gate
+ if: matrix.graph == 'jakarta-app'
+ env:
+ APP_PLUGINS: ${{ matrix.app_plugins }}
+ run: ./mvnw -B -ntp -f jakarta-test/pom.xml org.apache.maven.plugins:maven-dependency-plugin:3.11.0:resolve-plugins -DincludeArtifactIds="$APP_PLUGINS" -DoutputFile=target/build-dependencies.txt
- name: Submit build graph
env:
GH_TOKEN: ${{ github.token }}
@@ -71,7 +79,9 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
- ./mvnw -B -ntp -Psign-artifacts dependency:resolve-plugins -DoutputFile=target/build-dependencies.txt
+ ./mvnw -B -ntp -Psign-artifacts dependency:resolve-plugins \
+ -DincludeArtifactIds="${{ matrix.plugins }},maven-gpg-plugin,central-publishing-maven-plugin,maven-deploy-plugin" \
+ -DoutputFile=target/build-dependencies.txt
python3 scripts/build-dependency-snapshot.py --correlator encoder-build-release --output target/release-build-snapshot.json
gh api --method POST "repos/$GITHUB_REPOSITORY/dependency-graph/snapshots" --input target/release-build-snapshot.json
- name: Preserve resolved graphs
diff --git a/BUILDING.md b/BUILDING.md
index a0d5857..84eae3d 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -23,7 +23,7 @@ Run from the root, with `-pl core -am`, or from a module using `../mvnw`.
The wrapper's `.mvn` root also anchors Checkstyle paths. Maven's JVM must be 17+
(Maven 3.9.16+); Java 8 is a **forked unit-test/consumer JVM**, never the build JVM.
Newer JDK build jobs remain advisory. Library class files retain releases 8/9.
-The five library POMs share one Enforcer execution: tool minimums, duplicate
+The four release POMs share one Enforcer execution: tool minimums, duplicate
coordinates, dependency convergence, upper bounds and explicit plugin versions.
The separate Boot application applies those rules under its own parent/BOM.
@@ -37,7 +37,7 @@ requires Java 21; 12.3.1 is the explicit Java 17 compatibility exception, not a
claim of upstream support for older engines. Review migration when the build JDK
changes. Its parser cannot parse module declarations, so `module-info.java` is
excluded from Checkstyle; compiler, packaged descriptor/source guards and actual
-JPMS consumers cover it. Headers were added to those four descriptors and four
+JPMS consumers cover it. Headers were added to the three library descriptors and four
app files using their 2024 Jeremy Long introduction commits. All existing BSD
notices and original attribution remain. TLD license comments remain intact;
JSP server-side comments do not emit output. The app declares the same BSD license.
@@ -58,7 +58,6 @@ CI's Java 8 run starts in its own job/cache and uploads its own reports/data.
| core | 1228/1240 (99.032%) | 890/903 (98.560%) | 99.0% | 98.5% |
| jsp | 66/66 | no branches | 100% | 100% |
| jakarta | 66/66 | no branches | 100% | 100% |
-| esapi | 27/28 (96.429%) | no branches | 96.4% | 100% |
Floors round the current baseline down to 0.1 percentage points. They are not a
claim that every encoding behavior is covered. CI retains reports alongside test
@@ -95,7 +94,12 @@ Lifecycle pins are in root `pluginManagement`, with explicit versions on API,
source-helper and signature plugins. Maven 4 prerelease plugins were deliberately
not selected for this Maven 3 build. The optional app inherits maintained plugin
pins from Boot 4.1.1 and adds explicit Enforcer/Checkstyle/disabled Site pins.
-Review effective POMs for normal, `testJakarta`, and `sign-artifacts` profiles;
-`dependency:resolve-plugins` feeds their resolved closures into dependency review.
+Review effective POMs for normal, `testJakarta`, and `sign-artifacts` profiles.
+Dependency submission resolves a reviewed allowlist of the plugins those gates
+actually invoke and feeds their closures into dependency review. The optional
+Jakarta app has its own package-gate allowlist. Inactive plugin-management defaults
+and the disabled Site lifecycle are not represented as executed build dependencies;
+shared inherited tooling is submitted once at the root rather than copied onto
+every child POM.
Release-only publisher dependencies are submitted separately with the same
GitHub detector and a distinct correlator, so they do not hide runtime graphs.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cdcbeae..87b4521 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,19 +10,17 @@ unchanged. [1.4.1 is also available from Central](releases/1.4.1-central-publica
Development builds use `1.5.0-SNAPSHOT`; this is not a published release.
-* fix: resolve ESAPI's reference encoder lazily for each delegated operation. Missing configuration no longer poisons adapter initialization; OWASP-backed operations remain usable, and delegation can recover when configuration becomes available in the same JVM. JSON methods retain upstream delegation and null behavior [#165](https://github.com/OWASP/owasp-java-encoder/pull/165).
-
-* fix: the ESAPI adapter's `encodeForURL` now encodes individual URL components with UTF-8, including reserved delimiters and literal `+`, instead of preserving whole-URI delimiters. Spaces remain `%20`, null remains the string `"null"`, and unpaired surrogates remain `-`. See the [migration guide](esapi/README.md#url-encoding-migration-in-15-unreleased) for output changes, form-encoding differences, and the retained quoted HTML/CSS/JavaScript contracts [#100](https://github.com/OWASP/owasp-java-encoder/issues/100).
+* removed: retire the optional `encoder-esapi` adapter. Version 1.4.1 is its final published release and is no longer supported; no `encoder-esapi:1.5.0` artifact will be published. Consumers must remove the adapter and [migrate Java Encoder-backed calls to the direct context APIs](docs/encoder-esapi-retirement.md). Historical Maven artifacts remain immutable.
+* build: remove advisory-affected dependencies from active Maven plugin realms, invoke the same japicmp engine without its obsolete reporting wrapper, and submit only actually invoked build plugins to GitHub's dependency graph. Shared inherited tooling is recorded once, and no Dependabot alert is dismissed or suppressed.
* feat: all four `forJavaScript*` methods encode dollar sign (`$`) as `\x24`, backtick as `\x60`, and opening brace (`{`) as `\x7b` [#129](https://github.com/OWASP/owasp-java-encoder/issues/129). Escaping `{` prevents input after a trusted `$` from completing `${...}`. Encoded output now supports literal text in ordinary (untagged) template literals as well as single- and double-quoted strings. This changes the encoded output while preserving its decoded JavaScript string value. Tagged templates (including `String.raw`), `${...}` expression bodies, JSON, and script URLs are unsupported; each method's HTML context restrictions still apply.
* fix: all four `forJavaScript*` methods escape unpaired UTF-16 surrogates as `\uXXXX`, preserving their JavaScript string values through UTF-8 serialization [#135](https://github.com/OWASP/owasp-java-encoder/issues/135), and escape DEL/C1 controls (U+007F to U+009F) as `\xNN` [#163](https://github.com/OWASP/owasp-java-encoder/issues/163). Valid surrogate pairs and other non-ASCII text remain unescaped except U+2028/U+2029. These are output-fidelity changes; NEL was already ordinary JavaScript string data.
* fix: the HTML/block JavaScript encoders escape every ASCII character that can contribute to a case-insensitive `` script tokens, including the HTML end-tag delimiters, so any nonempty encoded substring cannot complete a delimiter supplied partly by adjacent trusted literal text. `forCDATA` represents every `]` and `>` with close/reopen sequences, preserving parsed text while breaking every nonempty encoded substring of `]]>`; its String facade grows with actual output instead of eagerly reserving the 13× maximum. `forXmlComment` replaces every hyphen with `~`. These are substantial compatibility-visible output changes; see the [migration record](docs/compatibility-decisions.md#15-parser-boundary-output-migration).
* fix: `EncodedWriter` now enforces Writer lifecycle semantics: write, append and flush operations fail after close; repeated close is harmless; the first close finalizes pending input and still attempts the delegate close, preserving simultaneous failures with suppressed exceptions. Array-slice writes now use overflow-safe bounds validation, including `Integer.MAX_VALUE`-shaped ranges.
-* build: compare all published artifacts against the immutable 1.4.1 public-API baseline, and verify that every publishable effective POM inherits repository-root SCM connection, developer connection and URL values without module-name suffixes.
-* dependencies: `encoder-esapi` overrides Apache HttpClient to 5.6.4 and HttpCore/Core H2 to 5.4.4. ESAPI's transitive Commons Configuration 1.10 and Commons Lang 2.6 remain in the graph and require an explicit maintainer release disposition; the HTTP overrides do not constitute acceptance of those residual findings.
-* feat: add `Encode.forJson` String/Writer methods, the `json` encoder context, and `forJson` tags and EL functions in both JSP and Jakarta tag libraries [#145](https://github.com/OWASP/owasp-java-encoder/issues/145). The caller supplies double quotes. Output uses RFC 8259 string escapes and also escapes HTML script delimiters. Java `null` becomes the text `null` (the JSON string `"null"` when quoted); unpaired surrogates use Unicode escapes and may not interoperate with every JSON consumer. Prefer a serializer for complete JSON documents. The ESAPI adapter retains its existing JSON delegation and null behavior.
+* build: compare all three 1.5.0 artifacts against the immutable 1.4.1 public-API baseline, and verify that every publishable effective POM inherits repository-root SCM connection, developer connection and URL values without module-name suffixes.
+* feat: add `Encode.forJson` String/Writer methods, the `json` encoder context, and `forJson` tags and EL functions in both JSP and Jakarta tag libraries [#145](https://github.com/OWASP/owasp-java-encoder/issues/145). The caller supplies double quotes. Output uses RFC 8259 string escapes and also escapes HTML script delimiters. Java `null` becomes the text `null` (the JSON string `"null"` when quoted); unpaired surrogates use Unicode escapes and may not interoperate with every JSON consumer. Prefer a serializer for complete JSON documents.
* feat: add `forXml11`, `forXml11Content` and `forXml11Attribute` tags and EL functions to the advanced JSP and Jakarta taglibs, and `forXml11` to the basic taglibs [#131](https://github.com/OWASP/owasp-java-encoder/issues/131).
* deprecation: `Encoders.URI` and both `ForUriTag` classes are now deprecated like `Encode.forUri`, whose Javadoc now says what to use instead; the `forUri` TLD descriptions warn about double encoding, the adapter builds show deprecation call sites, and the README has a [forUri migration section](README.md#migrating-from-foruri) [#130](https://github.com/OWASP/owasp-java-encoder/issues/130).
-* fix: the JSP, Jakarta and ESAPI bundles now declare the core versions they need (`[1.5,2)` for the tags, which call `Encode.forJson`; `[1.4.1,2)` for ESAPI) and the JSP API ranges they support, instead of unversioned imports that could wire to an older core and fail when a tag ran. Bundle symbolic names are now declared explicitly and unchanged [#137](https://github.com/OWASP/owasp-java-encoder/issues/137).
+* fix: the JSP and Jakarta bundles now declare the core version they need (`[1.5,2)`, because the tags call `Encode.forJson`) and the JSP API ranges they support, instead of unversioned imports that could wire to an older core and fail when a tag ran. Bundle symbolic names are now declared explicitly and unchanged [#137](https://github.com/OWASP/owasp-java-encoder/issues/137).
* fix: `forHtmlUnquotedAttribute` now replaces U+0085 (NEL) with a hyphen like the other C1 control characters, instead of emitting ` `, which HTML5 parsers decode as U+2026 [#136](https://github.com/OWASP/owasp-java-encoder/issues/136).
* fix: the XML 1.1 encoders (`forXml11`, `forXml11Content`, `forXml11Attribute`) now encode U+0085 (NEL) as ` ` and U+2028 (line separator) as ` `, so they are not normalized to a line feed [#136](https://github.com/OWASP/owasp-java-encoder/issues/136).
* maintenance: clarify output-context contracts and expand XML 1.1 tests, fix clean reactor compilation, and remove the obsolete benchmark profile.
@@ -42,8 +40,8 @@ Development builds use `1.5.0-SNAPSHOT`; this is not a published release.
and JDK 17 build policy, Checkstyle and measured unit coverage floors; isolate
signing/publishing tools, verify local bundles and measure reproducibility
(#185, #187). This does not change the Java 8 library runtime baseline.
-- Add release verification, historical key evidence, maintainer custody and
- release-specific ESAPI guidance (#164, #171, #185). Historical signing-key
+- Add release verification, historical key evidence, and maintainer custody
+ guidance (#164, #171, #185). Historical signing-key
authorization records (#110) now distinguish retrospective maintainer
authentication from historical GitHub/project records; see the
[key verification record](releases/historical-key-authentication.md).
@@ -83,7 +81,7 @@ these retained artifacts.
Add XML 1.1 core APIs (#88); update tests and security documentation (#83, #86, #87).
The ESAPI POM uses `[2.5.1.0,3)`; a temporary dependency pin controls resolution but
**does not fix Java Encoder's security issues**. See the
-[1.4.0 migration guidance](esapi/README.md#temporary-esapi-pin-for-140-consumers).
+[historical 1.4.1 adapter guidance](https://github.com/OWASP/owasp-java-encoder/blob/v1.4.1/esapi/README.md#temporary-esapi-pin-for-140-consumers).
## 1.3.1 — 2024-08-20
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 70bf448..325ea84 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -24,14 +24,7 @@ python3 compatibility/consumers.py run --runtime 17 --java-home "$JAVA_HOME"
Set `JAVA_HOME` to the JDK used for a consumer run. Prepare consumers only after a
successful reactor verify and in an empty `target/compatibility`; `clean` removes
-old preparation output. To exercise one ESAPI matrix version:
-
-```sh
-./mvnw -B -ntp -pl esapi -am clean verify -Desapi.version=2.7.0.0
-```
-
-Other supported versions and their upstream security status are separate in
-[esapi/README.md](esapi/README.md). See [BUILDING.md](BUILDING.md) for the verified
+old preparation output. See [BUILDING.md](BUILDING.md) for the verified
wrapper, Checkstyle's actual source scope/Java 17 exception, measured coverage
floors and diagnostics. The legacy Maven Site and benchmark profiles are retired.
Use fresh execution data when checking coverage; do not lower floors just to make
diff --git a/README.md b/README.md
index 589f87e..e217467 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,8 @@
Contextual output encoding for Java 8+. Choose an encoder for the parser context
receiving untrusted text: HTML, JavaScript, CSS, XML or a URL component. The core
-has no runtime dependencies; optional JSP, Jakarta and ESAPI adapters have their
-own dependency graphs. Encoding is one part of [XSS prevention][xss], alongside
+has no runtime dependencies; optional JSP and Jakarta adapters provide view-layer
+bindings. Encoding is one part of [XSS prevention][xss], alongside
safe templates, URL validation and other application controls.
**Upgrade all Java Encoder artifacts to 1.4.1. Versions through 1.4.0 are affected
@@ -17,20 +17,20 @@ and the signed [GitHub release][release]. All published artifacts and signatures
[VERIFYING.md](VERIFYING.md) for verification instructions.
`main` is **unreleased 1.5.0-SNAPSHOT**. Its JSON API, JavaScript template support,
-XML 1.1 tag bindings and ESAPI URL change are described below with version labels;
-they are not features of the signed 1.4.1 release. See [CHANGELOG.md](CHANGELOG.md).
+XML 1.1 tag bindings, and parser-boundary fixes are described below with version
+labels; they are not features of the signed 1.4.1 release. See
+[CHANGELOG.md](CHANGELOG.md).
## Start using the OWASP Java Encoders
Select the dependency you need; Maven resolves version 1.4.1 from Central.
-All four use group ID `org.owasp.encoder` and version `1.4.1`:
+The three supported artifacts use group ID `org.owasp.encoder`:
| Artifact ID | Purpose and runtime dependencies |
| --- | --- |
| `encoder` | Core String/Writer API; no runtime dependencies |
| `encoder-jsp` | Legacy `javax` JSP tags/EL functions; core plus container-provided JSP API |
| `encoder-jakarta-jsp` | Jakarta JSP tags/EL functions; core plus container-provided Jakarta JSP API |
-| `encoder-esapi` | ESAPI `Encoder` adapter; core and ESAPI 2.7.0.0 with its transitive dependencies |
```xml
@@ -40,14 +40,16 @@ All four use group ID `org.owasp.encoder` and version `1.4.1`:
```
-Replace `encoder` with one adapter artifact ID when needed; each adapter brings
+Replace `encoder` with one tag adapter artifact ID when needed; each adapter brings
in core. Keep separately managed core/adapter versions aligned. Use **one** of the
javax or Jakarta taglib JARs: they share `org.owasp.encoder.tag` and must not coexist
-on the same classpath or module path. See [ESAPI dependency and migration
-policy](esapi/README.md), [runtime matrix](compatibility/README.md), and
+on the same classpath or module path. See the [runtime matrix](compatibility/README.md) and
[dependency/license inventory](docs/dependencies.md). Development snapshots are
not security releases or a substitute for the signed 1.4.1 artifacts.
+`encoder-esapi` was retired after 1.4.1 and will not be published or supported in
+1.5.0. Applications using it must [migrate away from the adapter](docs/encoder-esapi-retirement.md).
+
```java
import org.owasp.encoder.Encode;
@@ -118,12 +120,9 @@ fragment; validate complete URLs separately, then use `forHtmlAttribute` when
placing one in a quoted HTML attribute. Parsing with `java.net.URI` alone does
not establish safety. See the [worked URL example](docs/usage.md#urls).
-The ESAPI adapter's `encodeForURL` changes in **unreleased 1.5** to component
-encoding, escaping delimiters and literal `+`, with `%20` spaces. Earlier adapter
-releases preserve whole-URI delimiters. Its existing null and malformed-Unicode
-policies remain. Read the [adapter migration guide](esapi/README.md#url-encoding-migration-in-15-unreleased)
-before upgrading. Removing the legacy API needs a separately reviewed future-major
-decision; deprecation is not a removal schedule.
+Removing the deprecated core/tag API needs a separately reviewed future-major
+decision; deprecation is not a removal schedule. The separate historical ESAPI
+adapter is retired rather than carried into 1.5.0.
## Java 9+ module names and OSGi
@@ -149,8 +148,8 @@ required in CI. See [BUILDING.md](BUILDING.md) for style/coverage policy,
[RELEASING.md](RELEASING.md) for the distinct release gates. There is no benchmark
or Maven Site publishing profile.
-The 1.x line preserves Java 8 library APIs/bytecode, published API/module identities
-and dependency scopes. Exact encoded output is also observable behavior: adding
+The supported 1.x artifacts preserve Java 8 library APIs/bytecode and their
+published API/module identities and dependency scopes. Exact encoded output is also observable behavior: adding
escapes is not automatically patch-compatible. Changes need context/parser tests,
an output-change note and migration guidance when required. Security fixes can
correct unsafe behavior in a patch with explicit advisories; other compatibility
diff --git a/RELEASING.md b/RELEASING.md
index 5ce1cf3..cf59fb2 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -83,9 +83,9 @@ existing Maven version.
[BUILDING.md](BUILDING.md) on the original JARs. With a running
Docker-compatible runtime, also run `./mvnw -B -ntp
-Dmaven.repo.local= verify -PtestJakarta`.
-6. Commit the release files before tagging. Verify the four binary JARs, their
- source and Javadoc JARs, and five POMs. The optional `jakarta-test` WAR is not a
- published component. Inspect all four generated manifests and effective parent/
+6. Commit the release files before tagging. Verify the three binary JARs, their
+ source and Javadoc JARs, and four POMs. The optional `jakarta-test` WAR is not a
+ published component. Inspect all three generated manifests and effective parent/
module POMs: current organization/maintainer IDs, project URL, Bundle-Vendor and
Bundle-DocURL, preserved original-author attribution, and unchanged published
JPMS/automatic/OSGi identities and dependency scopes. Metadata edits affect
@@ -120,7 +120,7 @@ python3 scripts/package-release.py --output /private/path/release-bundle.zip \
--fingerprint 1C5F632B86809F2F5DB25092BEA0075F94074A9B
```
-The assembler verifies all seventeen signatures against that expected fingerprint,
+The assembler verifies all thirteen signatures against that expected fingerprint,
checks the signed POMs match the source POMs, excludes the optional WAR, generates
four checksum types and refuses to overwrite an existing bundle. It never builds,
signs or uploads. Do **not** use `skipPublishing=true` as a bundle-generation
@@ -155,10 +155,10 @@ uploading. Keep an audit record of the exact uploaded bundle and its SHA-256.
4. Set the fixed versions in the security advisories and publish the advisories
in coordination with the available release. Do not announce a Central version
that has not actually published.
-5. Set main to the next development version (currently `1.5.0-SNAPSHOT` for the
- new JSON API), update
- `jakarta-test` accordingly, and reset the SCM tag to `HEAD`. README examples
- and the supported-version table continue to refer to the published release.
+5. Set main to the next unreleased version (for example `1.5.1-SNAPSHOT` or
+ `1.6.0-SNAPSHOT`, chosen through version review), update `jakarta-test`
+ accordingly, and reset the SCM tag to `HEAD`. README examples and the
+ supported-version table continue to refer to the published release.
6. Verify GitHub CI on main, update the OWASP project page, and check javadoc.io
after its indexing delay.
@@ -166,11 +166,11 @@ If staging fails, repair the cause and drop the failed staging deployment before
retrying. For a retained signed release awaiting delivery, correct access or
upload problems and retry the exact bundle; do not rebuild or re-sign it to
address a validation failure. Escalate a failure requiring different artifact bytes to
-the release coordinator. After publication, compare all four libraries' binary,
-source, and Javadoc JARs and all five POMs and their signatures from Central with
+the release coordinator. After publication, compare all three libraries' binary,
+source, and Javadoc JARs and all four POMs and their signatures from Central with
the retained files and signed checksums. Only after that comparison succeeds,
reconcile the Central-pending notices in README, SECURITY.md, release notes,
-the GitHub release, and the ESAPI consumer guidance. Check the OWASP project page
+and the GitHub release. Check the OWASP project page
and javadoc.io against the actual publication status as well.
Published Maven coordinates are immutable. If release tags are
@@ -208,8 +208,8 @@ limited, audit-visible emergency PR review bypass are documented in
Use the reference toolchain above, the exact immutable source commit, and its
recorded timestamp. `python3 scripts/check-reproducible.py --commit
--directory ` exports that commit twice, uses separate fresh
-Maven repositories, builds the twelve binary/source/Javadoc JARs and installs the
-five POMs locally, then compares all seventeen files directly by SHA-256. It never
+Maven repositories, builds the nine binary/source/Javadoc JARs and installs the
+four POMs locally, then compares all thirteen files directly by SHA-256. It never
signs or uploads. The initial experiment is recorded in the batch 04 validation
record. Compare the same source revision, never a different historical release.
OS/architecture, locale, archive permissions and the JDK distribution/version are
diff --git a/SECURITY.md b/SECURITY.md
index ffaa227..7f37ffc 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -14,7 +14,10 @@ older release lines are not patched.
| `org.owasp.encoder:encoder` | 1.4.1 | < 1.4.1 |
| `org.owasp.encoder:encoder-jsp` | 1.4.1 | < 1.4.1 |
| `org.owasp.encoder:encoder-jakarta-jsp` | 1.4.1 | < 1.4.1 |
-| `org.owasp.encoder:encoder-esapi` | 1.4.1 | < 1.4.1 |
+
+The optional `org.owasp.encoder:encoder-esapi` artifact is retired. Version
+1.4.1 is its final published release, no version is currently supported, and no
+1.5.0 artifact will be published. See the [retirement and migration notice](docs/encoder-esapi-retirement.md).
Upgrading the core `encoder` artifact to the latest 1.x release needs no code changes:
no public API was removed between 1.2.3 and 1.4.1. It does need Java 8 or later;
@@ -53,6 +56,7 @@ Out of scope:
- using an encoder in a context it does not document (for example, `forHtml` output
placed in a JavaScript string)
- canonicalization, decoding, or input validation, which this library does not perform
+- the retired `encoder-esapi` adapter
- vulnerabilities in ESAPI, Spring, or servlet containers; report ESAPI issues through
the [ESAPI security policy](https://github.com/ESAPI/esapi-java-legacy/security)
- the unpublished `jakarta-test` application
diff --git a/compatibility/README.md b/compatibility/README.md
index fa0319b..44b4573 100644
--- a/compatibility/README.md
+++ b/compatibility/README.md
@@ -11,34 +11,29 @@ and OSGi consumers. Java 9+ legs additionally run explicit and automatic modules
| `encoder` | Java 8 | No runtime dependencies |
| `encoder-jsp` | Java 8 | JSP 2.2.1, Servlet 3.0.1, EL 2.2.5 (`javax`) |
| `encoder-jakarta-jsp` | Java 8 with compatible container APIs | JSP 3.0.0, Servlet 5.0.0, EL 4.0.0 (`jakarta`) |
-| `encoder-esapi` | Java 8 with compatible ESAPI dependencies | ESAPI 2.7.0.0, HttpClient 5.6.4, HttpCore/Core H2 5.4.4, and remaining runtime dependencies |
The Jakarta fixture deliberately uses Servlet 5.0, whose minimum Java SE version
is 8 ([specification](https://jakarta.ee/specifications/servlet/5.0/)). Newer servlet
containers/APIs can require a newer JVM. The reactor's Jakarta tests use Servlet 6,
and the Docker/Selenium application remains in the separate JDK 17 `Java CI` job.
-This smoke matrix is not certification of every container, ESAPI operation, or
-transitive dependency on every JDK. The separate ESAPI version matrix tests the
-adapter's broader supported ESAPI range.
+This smoke matrix is not certification of every container or transitive
+dependency on every JDK.
-The Java 8 proof includes packaged consumer execution across all four artifacts,
+The Java 8 proof includes packaged consumer execution across all three artifacts,
including Jakarta with Java 8-compatible APIs. A separate CI job builds on JDK 17,
-then forks the core, JSP, and ESAPI unit tests on Temurin 8 and requires a JaCoCo
+then forks the core and JSP unit tests on Temurin 8 and requires a JaCoCo
execution-data file from each module. The Java 8 unit job does not run the
packaged/module-path integration tests or the Jakarta test suite; those remain on
-JDK 17. The isolated packaged consumer job covers those four artifacts on Java 8.
+JDK 17. The isolated packaged consumer job covers those three artifacts on Java 8.
## What runs
-`consumers.py prepare` copies the four JARs produced by `./mvnw clean verify`, resolves
+`consumers.py prepare` copies the three JARs produced by `./mvnw clean verify`, resolves
the pinned fixture dependencies, and compiles consumers independently of reactor
classes or test classpaths. Core consumers assert String and Writer output,
including input that crosses internal buffer boundaries, plus JavaScript and URI
encoding. JSP consumers instantiate the actual tag, set a minimal `JspContext`,
-call `doTag()`, and assert output. The ESAPI consumer obtains the real adapter and
-asserts an HTML encoding result. Its minimal `config/ESAPI.properties` is supplied
-explicitly; it is not a production configuration or a claim that ESAPI needs no
-configuration. SLF4J may report that the fixture has no logging provider.
+call `doTag()`, and assert output.
The runtime jobs download the prepared fixtures into fresh checkouts, verify the
original JAR SHA-256 values, assert the actual JVM specification version, and run
@@ -53,15 +48,13 @@ consumers assert the historical automatic
names documented in the root README. Because javac does not use that runtime
property for module discovery, preparation makes visibly separate
`compile-only-automatic/` copies without module descriptors. These copies are used
-only to compile the automatic consumers, never on a runtime path. The ESAPI JAR
-is on the module path; its other dependencies remain on the classpath to avoid
-unrelated split packages among legacy dependency JARs.
+only to compile the automatic consumers, never on a runtime path.
OSGi tests start Felix 5.6.12 (R6) and 7.0.5 (R8), install the actual core/adapter
JARs and a consumer probe bundle, assert ACTIVE state, invoke encoding through
the probe's bundle class loader, and shut down the framework. The framework host
-supplies the pinned servlet/JSP/EL or ESAPI API packages via system-package exports,
-at the versions the pinned API JARs declare (ESAPI packages are unversioned). The
+supplies the pinned servlet/JSP/EL API packages via system-package exports at the
+versions the pinned API JARs declare. The
JSP and Jakarta probes also run `ForJsonTag`, which needs the 1.5 core API. Each
adapter is then installed with the released 1.4.0 core and must fail to resolve,
proving its `org.owasp.encoder` import range excludes cores it cannot run on.
@@ -72,7 +65,7 @@ disabled because this fixture does not use them.
## Guards and limits
-Preparation asserts all four artifacts' automatic/explicit module names,
+Preparation asserts all three artifacts' automatic/explicit module names,
descriptor requirements (including transitive API readability), exports, OSGi
identities, imported packages with their exact version ranges, export versions, absence of execution-environment
requirements, multi-release layout, Java 8 class versions, TLD identities and
@@ -92,9 +85,9 @@ reach the `verify` phase.
There are no API exclusions today. A future intentional tag-package move requires
an explicit compatibility decision. If approved, add narrowly scoped japicmp
-`parameter/excludes/exclude` entries for the affected classes in the parent POM,
-with an issue link and migration notes; do not disable compatibility checks for
-the whole adapter. New public API members must carry `@since` for their first
+`--exclude` arguments for the affected classes in the parent POM, with an issue
+link and migration notes; do not disable compatibility checks for a whole artifact.
+New public API members must carry `@since` for their first
release. The XML 1.1 additions already carry `@since 1.4.0`.
JDK 21 and 25 additionally run advisory builds to expose compiler/plugin drift.
@@ -146,7 +139,6 @@ new 1.5 calls and must not be projected onto older published JARs.
| encoder | owasp.encoder | org.owasp.encoder |
| encoder-jakarta-jsp | owasp.encoder.jakarta | org.owasp.encoder.jakarta |
| encoder-jsp | owasp.encoder.jsp | org.owasp.encoder.jsp |
-| encoder-esapi | owasp.encoder.esapi | org.owasp.encoder.esapi |
The multi-release descriptors define the explicit Java 9+ module names. The
manifest names intentionally retain their historical values for consumers that
@@ -158,17 +150,13 @@ The adapter modules also require their public API dependency on the module path:
|---------------------------|----------------------------|-------------------------------------------------------|
| `owasp.encoder.jsp` | `javax.servlet.jsp.api` | `javax.servlet.jsp:javax.servlet.jsp-api:2.2.1` |
| `owasp.encoder.jakarta` | `jakarta.servlet.jsp` | `jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.0.0` |
-| `owasp.encoder.esapi` | `esapi` | `org.owasp.esapi:esapi:2.7.0.0` |
These dependencies are transitive in the module descriptors because their types
-appear in the adapters' public APIs. The JSP and ESAPI dependencies are automatic
-modules; use the original Maven artifact filenames so Java derives the module
+appear in the adapters' public APIs. The JSP dependencies are automatic modules;
+use the original Maven artifact filenames so Java derives the module
names shown above. Servlet containers continue to provide the JSP APIs at runtime,
and classpath-based applications are unaffected.
-The ESAPI adapter's fixed dependency and tested compatibility policy are
-documented in [esapi/README.md](../esapi/README.md).
-
### OSGi bundles
@@ -177,14 +165,9 @@ documented in [esapi/README.md](../esapi/README.md).
| encoder | `org.owasp.encoder` | `org.owasp.encoder` | (none) | (none) |
| encoder-jsp | `org.owasp.encoder.jsp` | `org.owasp.encoder.tag` | `[1.5,2)` | `javax.servlet.jsp`, `javax.servlet.jsp.tagext`: `[2.0,3)` |
| encoder-jakarta-jsp | `org.owasp.encoder.jakarta-jsp` | `org.owasp.encoder.tag` | `[1.5,2)` | `jakarta.servlet.jsp`, `jakarta.servlet.jsp.tagext`: `[3.0,4)` |
-| encoder-esapi | `org.owasp.encoder.esapi` | `org.owasp.encoder.esapi`| `[1.4.1,2)` | `org.owasp.esapi.*`: unversioned |
The symbolic names are fixed; note that the Jakarta bundle's differs from its
`Automatic-Module-Name`. Core exports `org.owasp.encoder` at its release version.
-The JSP and Jakarta tags require core 1.5 because they call `Encode.forJson`; the
-ESAPI adapter only calls older methods, so its floor is the oldest supported core,
-the 1.4.1 security release. Jakarta Pages 4 is not in the accepted range until
-compatibility with it has been verified. ESAPI publishes no OSGi metadata: OSGi
-users must wrap ESAPI and its dependencies as bundles themselves. The project's
-tests supply the ESAPI packages from the framework host, which is not a statement
-that upstream ESAPI supports OSGi.
+The JSP and Jakarta tags require core 1.5 because they call `Encode.forJson`.
+Jakarta Pages 4 is not in the accepted range until compatibility with it has been
+verified.
diff --git a/compatibility/config/ESAPI.properties b/compatibility/config/ESAPI.properties
deleted file mode 100644
index 7518483..0000000
--- a/compatibility/config/ESAPI.properties
+++ /dev/null
@@ -1,13 +0,0 @@
-# Minimal consumer fixture only; not a production ESAPI configuration.
-ESAPI.Encoder=org.owasp.encoder.esapi.ESAPIEncoder
-Encoder.AllowMultipleEncoding=false
-Encoder.AllowMixedEncoding=false
-Encoder.DefaultCodecList=HTMLEntityCodec,PercentCodec,JavaScriptCodec
-ESAPI.Logger=org.owasp.esapi.logging.slf4j.Slf4JLogFactory
-Logger.ApplicationName=Packaged-Encoder-Consumer
-Logger.LogEncodingRequired=false
-Logger.LogApplicationName=true
-Logger.LogServerIP=false
-Logger.UserInfo=false
-Logger.ClientInfo=false
-Logger.LogPrefix=true
diff --git a/compatibility/config/validation.properties b/compatibility/config/validation.properties
deleted file mode 100644
index f623896..0000000
--- a/compatibility/config/validation.properties
+++ /dev/null
@@ -1 +0,0 @@
-# This fixture exercises encoding only; it defines no validation rules.
diff --git a/compatibility/consumers.py b/compatibility/consumers.py
index 458a973..0c9b1b2 100644
--- a/compatibility/consumers.py
+++ b/compatibility/consumers.py
@@ -20,32 +20,21 @@
'core': ('encoder', 'owasp.encoder', 'org.owasp.encoder', 'org.owasp.encoder', 'CoreConsumer'),
'jsp': ('encoder-jsp', 'owasp.encoder.jsp', 'org.owasp.encoder.jsp', 'org.owasp.encoder.tag', 'TagConsumer'),
'jakarta': ('encoder-jakarta-jsp', 'owasp.encoder.jakarta', 'org.owasp.encoder.jakarta', 'org.owasp.encoder.tag', 'TagConsumer'),
- 'esapi': ('encoder-esapi', 'owasp.encoder.esapi', 'org.owasp.encoder.esapi', 'org.owasp.encoder.esapi', 'EsapiConsumer'),
}
API_MODULES = {'core': [], 'jsp': ['javax.servlet.jsp.api', 'javax.el.api', 'javax.servlet.api'],
- 'jakarta': ['jakarta.servlet.jsp', 'jakarta.el', 'jakarta.servlet'], 'esapi': ['esapi']}
+ 'jakarta': ['jakarta.servlet.jsp', 'jakarta.el', 'jakarta.servlet']}
HOST_PACKAGES = {'core': [], 'jsp': ['javax.servlet.jsp', 'javax.servlet.jsp.tagext', 'javax.servlet.jsp.el', 'javax.el'],
- 'jakarta': ['jakarta.servlet.jsp', 'jakarta.servlet.jsp.tagext', 'jakarta.servlet.jsp.el', 'jakarta.el'],
- 'esapi': ['org.owasp.esapi', 'org.owasp.esapi.codecs', 'org.owasp.esapi.errors', 'org.owasp.esapi.reference']}
+ 'jakarta': ['jakarta.servlet.jsp', 'jakarta.servlet.jsp.tagext', 'jakarta.servlet.jsp.el', 'jakarta.el']}
# Versions the framework exports for host packages, copied from the Export-Package
-# headers of the API JARs in compatibility/dependencies (ESAPI has no OSGi metadata).
+# headers of the API JARs in compatibility/dependencies.
HOST_VERSIONS = {'javax.servlet.jsp': '2.2.1', 'javax.servlet.jsp.tagext': '2.2.1', 'javax.servlet.jsp.el': '2.2.1',
'javax.el': '2.2.5', 'jakarta.servlet.jsp': '3.0.0.SNAPSHOT', 'jakarta.servlet.jsp.tagext': '3.0.0.SNAPSHOT',
'jakarta.servlet.jsp.el': '3.0.0.SNAPSHOT', 'jakarta.el': '4.0.0'}
-# Published Import-Package version ranges (#137); None means deliberately unversioned.
-# The tags call Encode.forJson (1.5); the ESAPI adapter's floor is the oldest supported core.
+# Published Import-Package version ranges (#137). The tags call Encode.forJson (1.5).
IMPORT_RANGES = {
'core': {},
'jsp': {'org.owasp.encoder': '[1.5,2)', 'javax.servlet.jsp': '[2.0,3)', 'javax.servlet.jsp.tagext': '[2.0,3)'},
'jakarta': {'org.owasp.encoder': '[1.5,2)', 'jakarta.servlet.jsp': '[3.0,4)', 'jakarta.servlet.jsp.tagext': '[3.0,4)'},
- 'esapi': {'org.owasp.encoder': '[1.4.1,2)', 'org.owasp.esapi': None, 'org.owasp.esapi.codecs': None,
- 'org.owasp.esapi.errors': None, 'org.owasp.esapi.reference': None},
-}
-
-ESAPI_HTTP_MINIMUMS = {
- ('org.apache.httpcomponents.client5', 'httpclient5'): (5, 6, 3),
- ('org.apache.httpcomponents.core5', 'httpcore5'): (5, 4, 3),
- ('org.apache.httpcomponents.core5', 'httpcore5-h2'): (5, 4, 3),
}
@@ -67,38 +56,6 @@ def clauses(value):
return re.split(r',(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)', value) if value else []
-def dependency_versions(pom, path):
- """Read literal versions for the HTTP dependencies at a POM path."""
- versions = {}
- for dependency in pom.findall(path, POM_NS):
- coordinate = (dependency.findtext('p:groupId', namespaces=POM_NS),
- dependency.findtext('p:artifactId', namespaces=POM_NS))
- if coordinate in ESAPI_HTTP_MINIMUMS:
- version = dependency.findtext('p:version', namespaces=POM_NS)
- assert version and '${' not in version, (coordinate, version)
- versions[coordinate] = version
- assert set(versions) == set(ESAPI_HTTP_MINIMUMS), versions
- return versions
-
-
-def validate_esapi_http_versions(published, fixture=None):
- """Keep consumer evidence aligned with the published, patched graph."""
- if fixture is not None:
- assert fixture == published, ('ESAPI consumer fixture differs from published POM',
- fixture, published)
- for coordinate, minimum in ESAPI_HTTP_MINIMUMS.items():
- version = published[coordinate]
- assert re.match(r'^\d+(?:\.\d+)*$', version), (coordinate, version)
- actual = tuple(int(part) for part in version.split('.'))
- assert actual[0] == minimum[0] and actual >= minimum, (
- coordinate, version, 'minimum supported', '.'.join(map(str, minimum)))
-
-
-def esapi_http_jars(versions):
- return {coordinate[1] + '-' + version + '.jar'
- for coordinate, version in versions.items()}
-
-
def manifest(jar):
with zipfile.ZipFile(jar) as archive:
text = archive.read('META-INF/MANIFEST.MF').decode().replace('\r\n', '\n').replace('\n ', '')
@@ -126,7 +83,6 @@ def metadata(kind, jar, core):
actual_ranges[name] = versions[0] if versions else None
assert actual_ranges == IMPORT_RANGES[kind], (kind, imports)
assert not any(x.startswith('java.') for x in imports), imports
- http_versions = None
with zipfile.ZipFile(jar) as archive, zipfile.ZipFile(core) as core_archive:
names = archive.namelist()
assert len(names) == len(set(names)), (jar, 'duplicate ZIP entries')
@@ -169,40 +125,20 @@ def metadata(kind, jar, core):
assert attrs['Bundle-Version'] == version.replace('-SNAPSHOT', '.SNAPSHOT'), attrs
runtime_dependencies = set()
provided_dependencies = set()
- direct_http_versions = {}
for dep in pom.findall('p:dependencies/p:dependency', POM_NS):
group = dep.findtext('p:groupId', namespaces=POM_NS)
scope = dep.findtext('p:scope', default='compile', namespaces=POM_NS)
coordinate = (group, dep.findtext('p:artifactId', namespaces=POM_NS))
- if coordinate in ESAPI_HTTP_MINIMUMS:
- direct_http_versions[coordinate] = dep.findtext(
- 'p:version', namespaces=POM_NS)
if scope in ('compile', 'runtime'):
runtime_dependencies.add(coordinate)
assert dep.findtext('p:optional', default='false', namespaces=POM_NS) == 'false', coordinate
if scope == 'provided': provided_dependencies.add(coordinate)
expected_dependencies = set() if kind == 'core' else {('org.owasp.encoder', 'encoder')}
- if kind == 'esapi':
- expected_dependencies.update({
- ('org.owasp.esapi', 'esapi'),
- ('org.apache.httpcomponents.client5', 'httpclient5'),
- ('org.apache.httpcomponents.core5', 'httpcore5'),
- ('org.apache.httpcomponents.core5', 'httpcore5-h2'),
- })
- http_versions = dependency_versions(
- pom, 'p:dependencyManagement/p:dependencies/p:dependency')
- validate_esapi_http_versions(http_versions)
- assert set(direct_http_versions) == set(ESAPI_HTTP_MINIMUMS), direct_http_versions
- for coordinate, direct_version in direct_http_versions.items():
- assert direct_version is None or direct_version == http_versions[coordinate], (
- coordinate, 'direct version overrides dependency management', direct_version,
- http_versions[coordinate])
assert runtime_dependencies == expected_dependencies, (kind, runtime_dependencies)
expected_provided = {'jsp': {('javax.servlet.jsp', 'javax.servlet.jsp-api')},
'jakarta': {('jakarta.servlet.jsp', 'jakarta.servlet.jsp-api')}}.get(kind, set())
assert provided_dependencies == expected_provided, (kind, provided_dependencies)
print('Metadata passed:', jar.name)
- return http_versions
def source_metadata(kind, source_jar):
@@ -224,7 +160,6 @@ def prepare(args):
if out.exists() and any(out.iterdir()):
raise ValueError('Preparation requires an empty directory; run ./mvnw clean verify or choose a new --directory: ' + str(out))
out.mkdir(parents=True, exist_ok=True)
- shutil.copytree(ROOT / 'compatibility/config', out / 'config', dirs_exist_ok=True)
parent = ET.parse(ROOT / 'pom.xml')
for dep in parent.findall('p:dependencies/p:dependency', POM_NS):
assert dep.findtext('p:scope', namespaces=POM_NS) == 'test', ET.tostring(dep)
@@ -240,16 +175,8 @@ def prepare(args):
source_metadata(kind, candidates[0].with_name(candidates[0].stem + '-sources.jar'))
with zipfile.ZipFile(candidates[0].with_name(candidates[0].stem + '-javadoc.jar')) as docs:
assert 'index.html' in docs.namelist(), ('missing Javadoc index', kind)
- published_http = None
for kind, jar in jars.items():
- result = metadata(kind, jar, jars['core'])
- if kind == 'esapi':
- published_http = result
- fixture_http = dependency_versions(
- ET.parse(ROOT / 'compatibility/dependencies/esapi.xml').getroot(),
- 'p:dependencies/p:dependency')
- validate_esapi_http_versions(published_http, fixture_http)
- expected_http_jars = esapi_http_jars(published_http)
+ metadata(kind, jar, jars['core'])
run('javac', '--release', '9', '-d', out / 'metadata', SOURCE / 'ModuleMetadata.java')
run('java', '-cp', out / 'metadata', 'consumer.ModuleMetadata', *jars.values())
# javac's module discovery does not honor the runtime multi-release property.
@@ -262,17 +189,13 @@ def prepare(args):
for entry in source.infolist():
if not entry.filename.endswith('module-info.class'):
target.writestr(entry, source.read(entry))
- for kind in ('jsp', 'jakarta', 'esapi', 'osgi-r6', 'osgi-r8', 'legacy-core'):
+ for kind in ('jsp', 'jakarta', 'osgi-r6', 'osgi-r8', 'legacy-core'):
run(args.maven, '-B', '-ntp', '-f', ROOT / 'compatibility/dependencies' / (kind + '.xml'),
'-Dmaven.repo.local=' + str(args.repository.resolve()),
'org.apache.maven.plugins:maven-dependency-plugin:3.11.0:copy-dependencies',
'-DincludeScope=runtime', '-DoutputDirectory=' + str(out / 'dependencies' / kind))
for kind, (artifact, explicit, automatic, package, main) in ARTIFACTS.items():
deps = sorted((out / 'dependencies' / kind).glob('*.jar'))
- if kind == 'esapi':
- actual_http = {item.name for item in deps
- if item.name.startswith(('httpclient5-', 'httpcore5-'))}
- assert actual_http == expected_http_jars, actual_http
artifacts = [jars['core']] + ([jars[kind]] if kind != 'core' else [])
src = out / 'sources' / kind
src.mkdir(parents=True, exist_ok=True)
@@ -288,7 +211,7 @@ def prepare(args):
module_src.parent.mkdir(exist_ok=True)
requires = [encoder_module] + API_MODULES[kind]
module_src.write_text('module consumer.fixture {\n' + ''.join(' requires ' + m + ';\n' for m in requires) + '}\n')
- module_deps = deps if kind != 'esapi' else [p for p in deps if p.name.startswith('esapi-')]
+ module_deps = deps
compile_artifacts = [compile_only / p.name for p in artifacts] if mode == 'automatic' else artifacts
run('javac', '--release', '9', '-d', out / mode / kind,
'--module-path', path(compile_artifacts + module_deps), module_src, *sources)
@@ -321,13 +244,12 @@ def consume(args):
for kind, (_, explicit, automatic, package, main) in ARTIFACTS.items():
deps = sorted((out / 'dependencies' / kind).glob('*.jar'))
artifacts = [jars['core']] + ([jars[kind]] if kind != 'core' else [])
- config = ['-Dorg.owasp.esapi.resources=' + str(out / 'config')] if kind == 'esapi' else []
- run(java, *config, '-cp', path([out / 'classes' / kind] + artifacts + deps), 'consumer.' + main)
+ run(java, '-cp', path([out / 'classes' / kind] + artifacts + deps), 'consumer.' + main)
if args.runtime != 8:
for mode, name in [('explicit', explicit), ('automatic', automatic)]:
- module_deps = deps if kind != 'esapi' else [p for p in deps if p.name.startswith('esapi-')]
+ module_deps = deps
classpath = [p for p in deps if p not in module_deps]
- run(java, *config, '-Djdk.util.jar.enableMultiRelease=' + str(mode == 'explicit').lower(),
+ run(java, '-Djdk.util.jar.enableMultiRelease=' + str(mode == 'explicit').lower(),
'-Dconsumer.module=' + name, '--module-path', path([out / mode / kind] + artifacts + module_deps),
'--class-path', path(classpath), '--module', 'consumer.fixture/consumer.' + main)
host = ','.join(p + (';version="' + HOST_VERSIONS[p] + '"' if p in HOST_VERSIONS else '')
@@ -336,14 +258,14 @@ def consume(args):
for framework in ('osgi-r6', 'osgi-r8'):
framework_jars = sorted((out / 'dependencies' / framework).glob('*.jar'))
with tempfile.TemporaryDirectory(prefix='encoder-osgi-') as storage:
- run(java, *config, '-cp', path([out / 'osgi'] + framework_jars + deps), 'consumer.OsgiConsumer',
+ run(java, '-cp', path([out / 'osgi'] + framework_jars + deps), 'consumer.OsgiConsumer',
storage, host, 'consumer.' + main,
*artifacts, out / 'probes' / (kind + '.jar'))
if kind != 'core':
# A released core older than the adapter's import range must not wire.
assert len(legacy_core) == 1, legacy_core
with tempfile.TemporaryDirectory(prefix='encoder-osgi-') as storage:
- run(java, *config, '-cp', path([out / 'osgi'] + framework_jars + deps), 'consumer.OsgiConsumer',
+ run(java, '-cp', path([out / 'osgi'] + framework_jars + deps), 'consumer.OsgiConsumer',
storage, host, '--expect-unresolved', legacy_core[0], jars[kind])
print('PASS Java', args.runtime, kind, flush=True)
diff --git a/compatibility/dependencies/esapi.xml b/compatibility/dependencies/esapi.xml
deleted file mode 100644
index 851ac23..0000000
--- a/compatibility/dependencies/esapi.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
- 4.0.0
- org.owasp.encoder.tests
- consumer-esapi
- 1
-
-
- org.owasp.esapi
- esapi
- 2.7.0.0
-
-
-
- org.apache.httpcomponents.client5
- httpclient5
- 5.6.4
-
-
- org.slf4j
- slf4j-api
-
-
-
-
- org.apache.httpcomponents.core5
- httpcore5
- 5.4.4
-
-
- org.apache.httpcomponents.core5
- httpcore5-h2
- 5.4.4
-
-
-
diff --git a/compatibility/src/EsapiConsumer.java b/compatibility/src/EsapiConsumer.java
deleted file mode 100644
index df01981..0000000
--- a/compatibility/src/EsapiConsumer.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (c) 2026 OWASP
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions
-// are met:
-//
-// * Redistributions of source code must retain the above
-// copyright notice, this list of conditions and the following
-// disclaimer.
-//
-// * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following
-// disclaimer in the documentation and/or other materials
-// provided with the distribution.
-//
-// * Neither the name of the OWASP nor the names of its
-// contributors may be used to endorse or promote products
-// derived from this software without specific prior written
-// permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
-// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
-// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
-// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-// OF THE POSSIBILITY OF SUCH DAMAGE.
-
-package consumer;
-
-public final class EsapiConsumer {
- public static void main(String[] args) throws Exception {
- Checks.origin(org.owasp.encoder.esapi.ESAPIEncoder.class);
- org.owasp.esapi.Encoder encoder = org.owasp.encoder.esapi.ESAPIEncoder.getInstance();
- Checks.encoded(encoder.encodeForHTML("A&B<"));
- if (!"a%20b%2Bc%26admin%3Dtrue%2F%23".equals(
- encoder.encodeForURL("a b+c&admin=true/#"))) {
- throw new AssertionError("URL component delimiters were not encoded");
- }
- if (!"null".equals(encoder.encodeForURL(null))) {
- throw new AssertionError("URL null contract changed");
- }
- }
-}
diff --git a/compatibility/src/ModuleMetadata.java b/compatibility/src/ModuleMetadata.java
index fc9d1e6..eb54216 100644
--- a/compatibility/src/ModuleMetadata.java
+++ b/compatibility/src/ModuleMetadata.java
@@ -11,9 +11,9 @@
/** Assert the actual packaged descriptors, including transitive API readability. */
public final class ModuleMetadata {
public static void main(String[] args) {
- String[] modules = {"owasp.encoder", "owasp.encoder.jsp", "owasp.encoder.jakarta", "owasp.encoder.esapi"};
- String[] packages = {"org.owasp.encoder", "org.owasp.encoder.tag", "org.owasp.encoder.tag", "org.owasp.encoder.esapi"};
- String[] apis = {null, "javax.servlet.jsp.api", "jakarta.servlet.jsp", "esapi"};
+ String[] modules = {"owasp.encoder", "owasp.encoder.jsp", "owasp.encoder.jakarta"};
+ String[] packages = {"org.owasp.encoder", "org.owasp.encoder.tag", "org.owasp.encoder.tag"};
+ String[] apis = {null, "javax.servlet.jsp.api", "jakarta.servlet.jsp"};
for (int i = 0; i < modules.length; ++i) {
ModuleDescriptor module = ModuleFinder.of(Paths.get(args[i])).find(modules[i]).orElseThrow(AssertionError::new).descriptor();
if (module.isAutomatic() || module.isOpen()) throw new AssertionError(module);
diff --git a/compatibility/tests/test_guards.py b/compatibility/tests/test_guards.py
index 0532af3..3a15cda 100644
--- a/compatibility/tests/test_guards.py
+++ b/compatibility/tests/test_guards.py
@@ -44,30 +44,6 @@ def test_original_packages_pass(self):
with self.subTest(artifact=kind):
consumers.metadata(kind, jar, self.jars['core'])
- def test_esapi_consumer_graph_matches_published_pom(self):
- published = consumers.metadata('esapi', self.jars['esapi'], self.jars['core'])
- fixture = consumers.dependency_versions(
- ET.parse(ROOT / 'compatibility/dependencies/esapi.xml').getroot(),
- 'p:dependencies/p:dependency')
- consumers.validate_esapi_http_versions(published, fixture)
-
- stale = dict(fixture)
- stale[('org.apache.httpcomponents.client5', 'httpclient5')] = '5.6.2'
- with self.assertRaises(AssertionError):
- consumers.validate_esapi_http_versions(published, stale)
-
- def test_esapi_direct_http_version_cannot_override_management(self):
- def mutate(entries):
- name = 'META-INF/maven/org.owasp.encoder/encoder-esapi/pom.xml'
- pom = ET.fromstring(entries[name])
- ns = '{http://maven.apache.org/POM/4.0.0}'
- for dependency in pom.findall(ns + 'dependencies/' + ns + 'dependency'):
- if dependency.findtext(ns + 'artifactId') == 'httpclient5':
- ET.SubElement(dependency, ns + 'version').text = '5.6.2'
- break
- entries[name] = ET.tostring(pom)
- self.rejected('esapi', mutate)
-
def test_osgi_execution_requirement(self):
def mutate(entries):
name = 'META-INF/MANIFEST.MF'
@@ -131,8 +107,8 @@ def test_unversioned_core_import(self):
entries, 'Import-Package', lambda value: value.replace('org.owasp.encoder;version="[1.5,2)"', 'org.owasp.encoder')))
def test_lowered_core_floor(self):
- self.rejected('esapi', lambda entries: self.header(
- entries, 'Import-Package', lambda value: value.replace('[1.4.1,2)', '[1.4,2)')))
+ self.rejected('jsp', lambda entries: self.header(
+ entries, 'Import-Package', lambda value: value.replace('[1.5,2)', '[1.4,2)')))
def test_widened_jakarta_pages_range(self):
self.rejected('jakarta', lambda entries: self.header(
diff --git a/core/pom.xml b/core/pom.xml
index c477b3d..3b29f48 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -128,6 +128,19 @@
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+
+ org.owasp.encoder
+ encoder
+ ${public.api.baseline.version}
+
+
+
diff --git a/docs/compatibility-decisions.md b/docs/compatibility-decisions.md
index 0e86b60..a98cdba 100644
--- a/docs/compatibility-decisions.md
+++ b/docs/compatibility-decisions.md
@@ -1,4 +1,4 @@
-# Compatibility and scope decisions — 2026-09-26
+# Compatibility and scope decisions — updated 2026-09-27
This record resolves the decision queue in [#142](https://github.com/OWASP/owasp-java-encoder/issues/142)
and [#149](https://github.com/OWASP/owasp-java-encoder/issues/149). It approves no
@@ -15,19 +15,20 @@ closed/not-planned proposals are not reopened. The full release gate in
| Jakarta tag package and split packages | **Keep `org.owasp.encoder.tag` in both separate adapters for 1.x; defer a rename.** | Both namespaces pass packaged JSP engines and parity checks. They cannot coexist on one classpath/module path. A rename could allow coexistence but would break direct class imports, reflective/TLD references and OSGi package wiring; it needs an explicit migration design before acceptance. |
| Deprecated `forUri` entry points | **Keep through all 1.x; defer any removal.** | `Encode.forUri` was already deprecated; 1.5 extends notices/annotations to registry and tags. Existing call sites retain behavior. Migrate raw components to `forUriComponent`, validate complete URLs, then encode the enclosing context. Removal would break source, binary and view-template callers and has no approved date. |
| Legacy javax JSP adapter | **Keep in 1.x; defer end-of-life or removal.** | Maintained javax Jasper fixtures and original-JAR consumers exercise it. Removing the artifact would strand applications whose container/framework migration is independent of this library. Passing fixtures are evidence of functionality, not a count of downstream users or a promise to support every old container. |
-| ESAPI adapter and dependency scope | **Keep the adapter and compile-scoped ESAPI dependency in 1.x; defer a scope/lifecycle change.** | Public APIs expose ESAPI types; its supported-version matrix and packaged consumers pass. Making ESAPI optional/provided would move dependency management to callers and could break compilation or runtime linkage. Upstream security support/advisories remain a separate review, not a blanket exemption. |
+| ESAPI adapter and dependency scope | **Retire the adapter; 1.4.1 is its final release and no version remains supported.** | This supersedes the 2026-09-26 plan to keep the adapter through 1.x. A newer ESAPI/Commons graph cannot be obtained with compatible version overrides, and maintaining another project's API, dependency graph, or a private ESAPI fork is outside Java Encoder's scope. No `encoder-esapi:1.5.0` artifact will be published; see the [migration notice](encoder-esapi-retirement.md). |
| Multi-release JARs versus one classfile level | **Keep Java 8 classes plus Java 9 descriptors in 1.x; defer single-release packaging.** | Original JARs pass Java 8 and explicit/automatic JPMS tests. A future runtime baseline of 9+ might permit one release level, but that depends on a baseline decision and all packaging/OSGi/JPMS evidence, not just compiler convenience. |
-| Exact encoded output and contexts | **Keep explicit versioning/migration review; accept no blanket “more escaping is patch-safe” rule.** | 1.5's ESAPI URL-component change alters delimiters; JavaScript escape additions preserve interpreted strings but change bytes. Output affects snapshots, signatures and protocol consumers. Security patches may correct unsafe behavior with an advisory; other changes need parser tests, release notes and a justified version. |
+| Exact encoded output and contexts | **Keep explicit versioning/migration review; accept no blanket “more escaping is patch-safe” rule.** | JavaScript escape additions preserve interpreted strings but change bytes. Output affects snapshots, signatures and protocol consumers. Security patches may correct unsafe behavior with an advisory; other changes need parser tests, release notes and a justified version. |
Evidence: [runtime matrix, package guards and identities](../compatibility/README.md),
[required browser/WAR fixture](../jakarta-test/README.md), [JSP engine checks](../compatibility/jsp-engine/README.md),
-[ESAPI contracts](../esapi/README.md), [migration guide](../README.md#migrating-from-foruri)
-and [merged-change history](../CHANGELOG.md). These are real consumer fixtures,
+[migration guide](../README.md#migrating-from-foruri), and
+[merged-change history](../CHANGELOG.md). These are real consumer fixtures,
not an ecosystem usage census; a few code-search hits cannot establish that a
breaking change has no downstream users.
-No breaking proposal above is accepted for implementation. Therefore no speculative
-implementation tickets or removals are created. To reconsider an item, maintainers
+Except for the explicit `encoder-esapi` retirement recorded above, no breaking
+proposal in this table is accepted for implementation. Therefore no speculative
+implementation tickets or removals are created. To reconsider another item, maintainers
must review a focused proposal with affected public surfaces, measured consumer
impact, migration examples, version choice and alternatives. An accepted break
must be announced in a published migration/design note and release notes **before**
@@ -67,7 +68,7 @@ canonical decoding require byte/protocol contracts that do not fit contextual
output encoding. In particular, a decoder broadens the existing non-goals, and
byte grouping does not fit the char-to-char `Encoder`/`EncodedWriter` abstraction.
Encoding-only view tags would add another surface without a demonstrated need;
-no decoder tags are accepted. Existing ESAPI delegates stay unchanged.
+no decoder tags are accepted.
For a protocol that explicitly requires **unpadded** base64url, use already-defined
bytes with the [JDK 8+ Base64 API](https://docs.oracle.com/javase/8/docs/api/java/util/Base64.html):
diff --git a/docs/contexts.md b/docs/contexts.md
index e3a4844..b41a3c9 100644
--- a/docs/contexts.md
+++ b/docs/contexts.md
@@ -98,11 +98,11 @@ element. Ordinary JSON serialization alone need not protect an HTML end tag.
Java `null` is encoded as the text `null`: with the caller's quotes this is the
JSON **string** `"null"`, not the JSON null value. The facade generally renders null
-as text; JSP EL conversion and the ESAPI-delegated JSON method have separate
-contracts. `forJson` preserves unpaired surrogates as `\uXXXX`, which not every
+as text; JSP EL conversion has its own contract. `forJson` preserves unpaired
+surrogates as `\uXXXX`, which not every
JSON consumer interoperates with. JSON strings do not belong in HTML attributes
without the enclosing attribute encoding, and HTML entity escaping is not JSON
-serialization. The ESAPI adapter keeps its existing upstream JSON delegation.
+serialization.
## URLs and non-goals
@@ -116,8 +116,7 @@ encoded for a surrounding quoted HTML attribute. See the [URL example](usage.md#
`forUriComponent` uses UTF-8 percent encoding and `%20` spaces. Form encoding
(`java.net.URLEncoder`) has a different contract. Already encoded input is encoded
again; unpaired surrogates are replaced with `-`. Deprecated `forUri` preserves
-whole-URI delimiters and cannot make a dangerous scheme safe. Read the
-[unreleased ESAPI URL migration](../esapi/README.md#url-encoding-migration-in-15-unreleased).
+whole-URI delimiters and cannot make a dangerous scheme safe.
Java Encoder does not perform input validation, HTML sanitization, URL authorization,
SQL parameterization, canonicalization or decoding. Its JSON and JavaScript
diff --git a/docs/dependencies.md b/docs/dependencies.md
index 27ecc08..e601e0d 100644
--- a/docs/dependencies.md
+++ b/docs/dependencies.md
@@ -1,79 +1,40 @@
# Dependencies and declared licenses
-The core `encoder` has **no runtime dependencies**. `encoder-jsp` and
+The core `encoder` artifact has **no runtime dependencies**. `encoder-jsp` and
`encoder-jakarta-jsp` depend on core and declare their matching JSP API as
-`provided`; the container supplies it. `encoder-esapi` has compile dependencies
-on core, ESAPI 2.7.0.0, and patched HTTP Components, including ESAPI's other
-transitives. None of these third-party classes is shaded into the four Encoder
-JARs. The optional Boot/WAR
-fixture, test engines and build plugins are development tooling, not published
-library runtime dependencies. Review the [live dependency graph](https://github.com/OWASP/owasp-java-encoder/network/dependencies)
-for those separate scopes and [ESAPI advisory triage](../esapi/README.md#dependency-security-triage).
+`provided`; the container supplies it. No third-party classes are shaded into
+the three Java Encoder 1.5.0 JARs.
+
+The optional Boot/WAR fixture, parser and JSP-engine tests, build plugins, and
+release tooling are development infrastructure. They are not dependencies of a
+published Java Encoder library. Review the [live dependency graph][graph] for
+their separate scopes.
+
+`encoder-esapi` was retired after version 1.4.1 and is absent from the 1.5.0
+reactor and dependency graph. Its historical artifact remains immutable but is
+unsupported; see the [retirement and migration notice](encoder-esapi-retirement.md).
## Consumer dependency inventory — 2026-09-27
Generated from dependency-plugin 3.11.0's resolved reactor `dependency:tree`
-JSON for current `1.5.0-SNAPSHOT`. Includes compile/runtime and provided scopes,
-excludes test/plugin dependencies and this project's own BSD-3-Clause modules.
-The scope column identifies the consuming module. These are **upstream POM
-license declarations**, following parent POM inheritance where necessary; the
-source links identify the exact declaring POM. They are not a legal conclusion
-about every file or application distribution. Preserve required notices and
-review the artifacts you actually distribute. The project license is [LICENSE](../LICENSE).
+JSON for current `1.5.0-SNAPSHOT`. This inventory includes the publishable
+modules' compile/runtime and provided scopes, excludes test/plugin dependencies
+and this project's own BSD-3-Clause modules, and identifies the consuming module.
```sh
./mvnw dependency:tree -DoutputType=json -DoutputFile=target/dependencies.json
```
-Dependency management in an application can resolve a different graph. Recheck
-the inventory after a version/scope change; a listed license says nothing about
-security support or advisory status. This inventory does not rewrite published
-1.4.1 POMs or claim that every third-party POM uses a normalized SPDX identifier.
-
| Coordinate | Consumer scope | Declared licenses | Provenance |
| --- | --- | --- | --- |
-| `commons-beanutils:commons-beanutils:1.11.0` | esapi: compile | Apache-2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/34/apache-34.pom) |
-| `commons-collections:commons-collections:3.2.2` | esapi: compile | Apache License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/16/apache-16.pom) |
-| `commons-configuration:commons-configuration:1.10` | esapi: compile | The Apache Software License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/13/apache-13.pom) |
-| `commons-fileupload:commons-fileupload:1.6.0` | esapi: compile | Apache-2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/34/apache-34.pom) |
-| `commons-io:commons-io:2.19.0` | esapi: compile | Apache-2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/33/apache-33.pom) |
-| `commons-lang:commons-lang:2.6` | esapi: compile | The Apache Software License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/7/apache-7.pom) |
-| `commons-logging:commons-logging:1.3.5` | esapi: compile | Apache-2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/33/apache-33.pom) |
| `jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.0.0` | jakarta: provided | Eclipse Public License v. 2.0; GNU General Public License, version 2 with the GNU Classpath Exception | [POM](https://repo.maven.apache.org/maven2/org/eclipse/ee4j/project/1.0.6/project-1.0.6.pom) |
-| `javax.servlet.jsp:javax.servlet.jsp-api:2.2.1` | jsp: provided | CDDL + GPLv2 with classpath exception | [POM](https://repo.maven.apache.org/maven2/javax/servlet/jsp/javax.servlet.jsp-api/2.2.1/javax.servlet.jsp-api-2.2.1.pom) |
-| `org.apache-extras.beanshell:bsh:2.0b6` | esapi: compile | Apache License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache-extras/beanshell/bsh/2.0b6/bsh-2.0b6.pom) |
-| `org.apache.commons:commons-collections4:4.5.0-M2` | esapi: compile | Apache-2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/32/apache-32.pom) |
-| `org.apache.httpcomponents.client5:httpclient5:5.6.4` | esapi: compile | Apache License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/27/apache-27.pom) |
-| `org.apache.httpcomponents.core5:httpcore5:5.4.4` | esapi: compile | Apache License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/27/apache-27.pom) |
-| `org.apache.httpcomponents.core5:httpcore5-h2:5.4.4` | esapi: compile | Apache License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/27/apache-27.pom) |
-| `org.apache.xmlgraphics:batik-constants:1.19` | esapi: compile | The Apache Software License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/xmlgraphics/batik/1.19/batik-1.19.pom) |
-| `org.apache.xmlgraphics:batik-css:1.19` | esapi: compile | The Apache Software License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/xmlgraphics/batik/1.19/batik-1.19.pom) |
-| `org.apache.xmlgraphics:batik-i18n:1.19` | esapi: compile | The Apache Software License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/xmlgraphics/batik/1.19/batik-1.19.pom) |
-| `org.apache.xmlgraphics:batik-shared-resources:1.19` | esapi: compile | The Apache Software License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/xmlgraphics/batik/1.19/batik-1.19.pom) |
-| `org.apache.xmlgraphics:batik-util:1.19` | esapi: compile | The Apache Software License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/xmlgraphics/batik/1.19/batik-1.19.pom) |
-| `org.apache.xmlgraphics:xmlgraphics-commons:2.11` | esapi: compile | The Apache Software License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/xmlgraphics/xmlgraphics-commons/2.11/xmlgraphics-commons-2.11.pom) |
-| `org.htmlunit:neko-htmlunit:4.11.0` | esapi: compile | Apache License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/htmlunit/neko-htmlunit/4.11.0/neko-htmlunit-4.11.0.pom) |
-| `org.owasp.antisamy:antisamy:1.7.8` | esapi: compile | BSD 3 | [POM](https://repo.maven.apache.org/maven2/org/owasp/antisamy/antisamy/1.7.8/antisamy-1.7.8.pom) |
-| `org.owasp.esapi:esapi:2.7.0.0` | esapi: compile | BSD; Creative Commons 3.0 BY-SA | [POM](https://repo.maven.apache.org/maven2/org/owasp/esapi/esapi/2.7.0.0/esapi-2.7.0.0.pom) |
-| `org.slf4j:slf4j-api:2.0.16` | esapi: compile | MIT License | [POM](https://repo.maven.apache.org/maven2/org/slf4j/slf4j-bom/2.0.16/slf4j-bom-2.0.16.pom) |
-| `xerces:xercesImpl:2.12.2` | esapi: compile | The Apache Software License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/xerces/xercesImpl/2.12.2/xercesImpl-2.12.2.pom) |
-| `xml-apis:xml-apis:1.4.01` | esapi: compile | The Apache Software License, Version 2.0; The SAX License; The W3C License | [POM](https://repo.maven.apache.org/maven2/xml-apis/xml-apis/1.4.01/xml-apis-1.4.01.pom) |
-| `xml-apis:xml-apis-ext:1.3.04` | esapi: compile | The Apache Software License, Version 2.0 | [POM](https://repo.maven.apache.org/maven2/org/apache/apache/3/apache-3.pom) |
-| `xom:xom:1.3.9` | esapi: compile | The GNU Lesser General Public License, Version 2.1 | [POM](https://repo.maven.apache.org/maven2/xom/xom/1.3.9/xom-1.3.9.pom) |
-
-The XML API declarations need component-level context. Their exact Central JARs
-provide more detailed notices than the POM metadata alone:
+| `javax.servlet.jsp:javax.servlet.jsp-api:2.2.1` | jsp: provided | CDDL + GPLv2 with Classpath Exception | [POM](https://repo.maven.apache.org/maven2/javax/servlet/jsp/javax.servlet.jsp-api/2.2.1/javax.servlet.jsp-api-2.2.1.pom) |
-- [`xml-apis:1.4.01`](https://repo.maven.apache.org/maven2/xml-apis/xml-apis/1.4.01/xml-apis-1.4.01.jar)
- contains `license/LICENSE` (Apache 2.0), `LICENSE.dom-software.txt` and
- `LICENSE.dom-documentation.txt` (W3C notices), `LICENSE.sax.txt` (the SAX
- public-domain statement), and `license/NOTICE`. The bundled `README.dom.txt`
- and `README.sax.txt` explain which files each notice covers.
-- [`xml-apis-ext:1.3.04`](https://repo.maven.apache.org/maven2/xml-apis/xml-apis-ext/1.3.04/xml-apis-ext-1.3.04.jar)
- contains `license/LICENSE` (Apache 2.0), DOM software/documentation notices,
- `LICENSE.sac.html` (W3C notice), `license/NOTICE` and `README.dom.txt`.
+These are upstream POM license declarations, not a legal conclusion about every
+file or application distribution. Preserve required notices and review the
+artifacts actually distributed. The Java Encoder project license is
+[BSD-3-Clause](../LICENSE). Dependency management in an application can resolve
+a different graph; recheck it after any version or scope change. A listed license
+says nothing about security support or advisory status.
-These notices cover different components; the list is not a claim that a consumer
-can choose any one license for the entire JAR. Several old declared license URLs
-are obsolete, so the table links to the retained declaring POM rather than
-substituting a new license or treating a dead URL as absence of a license.
+[graph]: https://github.com/OWASP/owasp-java-encoder/network/dependencies
diff --git a/docs/encoder-esapi-retirement.md b/docs/encoder-esapi-retirement.md
new file mode 100644
index 0000000..6276e26
--- /dev/null
+++ b/docs/encoder-esapi-retirement.md
@@ -0,0 +1,54 @@
+# Retirement of `encoder-esapi`
+
+The optional `org.owasp.encoder:encoder-esapi` adapter is retired. Version
+**1.4.1 is its final published release**. Starting with Java Encoder 1.5.0, this
+repository does not build, test, publish, or support an ESAPI adapter.
+
+The immutable 1.4.1 artifact remains available from Maven Central and its source
+remains in the [`v1.4.1` tag][historical-source]. It will not receive the security,
+correctness, or output-context fixes made in Java Encoder 1.5.0. In particular,
+consumers must not treat the historical adapter as a way to obtain the 1.5 parser-
+boundary fixes. Mixing `encoder-esapi:1.4.1` with a newer core is not a supported
+migration path.
+
+## Migrating
+
+Remove the `encoder-esapi` dependency and call the context-specific Java Encoder
+API directly for output encoding:
+
+| Historical adapter operation | Direct Java Encoder operation |
+| --- | --- |
+| HTML text | `Encode.forHtmlContent` |
+| Quoted HTML attribute | `Encode.forHtmlAttribute` |
+| Quoted CSS string | `Encode.forCssString` |
+| JavaScript string or ordinary untagged template text | `Encode.forJavaScript` |
+| One raw URL component | `Encode.forUriComponent` |
+
+Choose the method from the destination parser context; these are not blanket
+byte-for-byte replacements for every historical adapter output. Follow the
+[context guide](contexts.md) and [usage examples](usage.md), especially the URL
+assembly and validation requirements.
+
+The former adapter also implemented ESAPI operations outside Java Encoder's
+scope, including canonicalization, decoding, Base64, SQL, operating-system,
+LDAP, DN, XPath, VBScript, and JSON delegation. Applications needing those
+operations must select and maintain an appropriate upstream implementation.
+Java Encoder does not provide replacements for them and no longer supplies an
+`ESAPI.Encoder` implementation. Update any ESAPI configuration that names
+`org.owasp.encoder.esapi.ESAPIEncoder` according to ESAPI's own documentation.
+
+## Why it was retired
+
+The adapter exposed another project's API as its public contract and brought that
+project's dependency graph into every adapter consumer. The newest stable ESAPI
+release still depends on legacy Commons libraries for which no compatible patched
+line exists; newer Commons generations use different packages and APIs and cannot
+be substituted by changing version numbers. Maintaining a private ESAPI fork is
+outside Java Encoder's contextual-output-encoding scope.
+
+This retirement removes only the optional adapter coordinate. Java Encoder 1.5.0
+continues to publish and support `encoder`, `encoder-jsp`, and
+`encoder-jakarta-jsp`. Previously published Maven coordinates are immutable and
+will not be deleted or replaced.
+
+[historical-source]: https://github.com/OWASP/owasp-java-encoder/tree/v1.4.1/esapi
diff --git a/esapi/README.md b/esapi/README.md
deleted file mode 100644
index 05e6640..0000000
--- a/esapi/README.md
+++ /dev/null
@@ -1,229 +0,0 @@
-# ESAPI adapter dependency policy
-
-The ESAPI dependency depends on the `encoder-esapi` release you consume:
-
-| Adapter version | ESAPI dependency in its POM | Availability |
-| --- | --- | --- |
-| `1.4.0` | Maven range `[2.5.1.0,3)`; resolution can change and can select a release candidate | Maven Central; affected by Java Encoder's 1.4.1 security advisories |
-| `1.4.1` | Fixed default `2.7.0.0` | Maven Central and [signed GitHub security release][encoder-release] |
-| `1.5.0-SNAPSHOT` | Fixed default `2.7.0.0`, plus patched HTTP Components compile dependencies | Unreleased development; not a published release |
-
-The fixed dependency was introduced in 1.4.1. It does not change the POM already
-published for 1.4.0. As checked on 2026-09-27, upstream's [latest stable release][esapi-latest]
-and [security policy][esapi-security] identify ESAPI 2.7.0.0 as current and supported.
-Recheck those sources when choosing a version; adapter compatibility does not
-establish upstream security support.
-
-## Upgrade to 1.4.1
-
-Upgrade **all OWASP Java Encoder dependencies to 1.4.1**, including the core
-`encoder` if your application declares or manages it separately. Version 1.4.1
-resolves from Maven Central. The [signed GitHub artifacts][encoder-release]
-remain available with [verification instructions][encoder-verification].
-All Central artifacts and signatures [match the retained release](../releases/1.4.1-central-publication.md).
-
-```xml
-
- org.owasp.encoder
- encoder-esapi
- 1.4.1
-
-```
-
-## Temporary ESAPI pin for 1.4.0 consumers
-
-If you temporarily remain on `encoder-esapi:1.4.0`, add this to your application's
-POM (or merge it into its existing `dependencyManagement`) to replace the ESAPI
-range with a deterministic version:
-
-```xml
-
-
-
- org.owasp.esapi
- esapi
- 2.7.0.0
-
-
-
-```
-
-**Pinning ESAPI does not fix Java Encoder's security issues.** Version 1.4.0
-remains affected by [the three advisories fixed in 1.4.1][encoder-advisories].
-This pin only controls the ESAPI dependency; upgrade Java Encoder as well.
-Check the application's resolved graph with `mvn dependency:tree`, particularly
-if another dependency or BOM also manages ESAPI or the core encoder.
-
-## Tested adapter compatibility
-
-The supported compatibility range is the stable ESAPI releases from 2.5.1.0
-through 2.7.0.0, inclusive. CI builds and runs the adapter tests against every
-stable release in that range:
-
-- 2.5.1.0, 2.5.2.0, 2.5.3.0, and 2.5.3.1
-- 2.5.4.0 and 2.5.5.0
-- 2.6.0.0, 2.6.1.0, and 2.6.2.0
-- 2.7.0.0
-
-This is an adapter compatibility statement, not an upstream security-support
-statement. At the check date above, the [ESAPI security policy][esapi-security]
-supports only 2.7.0.0 and recommends upgrading from earlier versions. Passing
-adapter tests on an older ESAPI version does not make it supported upstream.
-
-Maintainers can deliberately test another version without changing the POM:
-
-```shell
-./mvnw -pl esapi -am clean verify -Desapi.version=2.5.1.0
-```
-
-Applications can select another tested version with normal Maven dependency
-management. The signed 1.4.1 POM and current development POM default
-deterministically to 2.7.0.0; the Central 1.4.0 POM still uses the range above.
-
-## URL encoding migration in 1.5 (unreleased)
-
-Starting with `1.5.0-SNAPSHOT`, `ESAPIEncoder.getInstance().encodeForURL(value)`
-uses `Encode.forUriComponent` to encode **one raw URL component** with UTF-8.
-Earlier adapter releases, including signed 1.4.1, use deprecated `Encode.forUri`
-and leave delimiters such as `& = / ? # +` unchanged. For example:
-
-| Raw input | Adapter through 1.4.1 | Adapter 1.5 | ESAPI reference with UTF-8 |
-| --- | --- | --- | --- |
-| `a b+c&admin=true` | `a%20b+c&admin=true` | `a%20b%2Bc%26admin%3Dtrue` | `a+b%2Bc%26admin%3Dtrue` |
-| `~*` | `~*` | `~%2A` | `%7E*` |
-| `%20` | `%2520` | `%2520` | `%2520` |
-| Java `null` | String `"null"` | String `"null"` | Java `null` |
-| Unpaired UTF-16 surrogate | `-` | `-` | `%3F` (replacement `?`) |
-
-The adapter deliberately uses RFC 3986 component encoding with `%20` for spaces,
-preserving its existing space, null, and malformed-Unicode policies. ESAPI's
-[reference implementation][esapi-url-reference] uses form encoding with `+` for
-spaces and the configured `Encryptor.CharacterEncoding`. Both escape URL
-delimiters, but their output is not interchangeable for byte comparisons or
-request signatures. Use `java.net.URLEncoder.encode(value, "UTF-8")` if a
-protocol requires form encoding. The adapter still declares `EncodingException`
-and does not read ESAPI configuration for this operation.
-
-Encode each raw parameter name/value or path segment once, then assemble the
-URL using trusted delimiters. Validate the final URL against application rules,
-including allowed schemes and any destination/path restrictions. Component
-encoding does not prevent every application-specific path issue (for example,
-`.` and `..` are unreserved). URI parsing alone is not a safety check. For a
-quoted HTML URL attribute, encode the assembled, validated URL with
-`Encode.forHtmlAttribute`. See the [shared migration guidance](../README.md#migrating-from-foruri).
-
-Callers that passed complete URLs must change that call pattern before adopting
-1.5: component encoding escapes the scheme colon, slashes, and other structural
-delimiters. Already percent-encoded input is encoded again. Existing core,
-registry, JSP, and Jakarta `forUri` entry points retain their behavior throughout
-1.x; only this adapter delegate changes. This is unreleased 1.5 behavior, not a
-change to the retained 1.4.1 artifacts.
-
-## Supported output contexts
-
-The adapter intentionally preserves these contexts rather than matching every
-escape emitted by ESAPI's reference implementation:
-
-| Method | Supported context and caller responsibility |
-| --- | --- |
-| `encodeForHTMLAttribute` | A quoted HTML text attribute. Supply single or double quotes. HTML escaping alone does not make event-handler code or an unvalidated URL safe. |
-| `encodeForCSS` | A quoted CSS string using `Encode.forCssString`; not arbitrary unquoted CSS values or expressions. |
-| `encodeForJavaScript` | A single/double-quoted string or literal text in an ordinary untagged template, using `Encode.forJavaScript`. Not JSON, tagged templates (including `String.raw`), `${...}` expression bodies, arbitrary unquoted code, or script URLs. |
-| `encodeForURL` (1.5) | One raw URL component; assemble and validate the URL, then encode for its enclosing context. |
-
-More escaping does not make arbitrary unquoted JavaScript or CSS safe. The CSS
-size fix, JavaScript template-boundary and Unicode handling, JSON delegation,
-lazy reference lookup, and ESAPI's default disablement of unsafe SQL encoding
-remain intact. Parser and contract tests run against the stable ESAPI matrix
-listed above; that matrix remains separate from upstream security support.
-
-## Runtime and security notes
-
-`ESAPIEncoder.getInstance()` and its OWASP Java Encoder-backed methods do not
-load ESAPI configuration. Delegated operations resolve ESAPI's reference encoder
-on each call, so a missing `ESAPI.properties` causes an ESAPI configuration
-exception for that operation and can be retried after configuration becomes
-available in the same JVM. ESAPI retains responsibility for caching its reference
-encoder. Obtaining the adapter through `ESAPI.encoder()` still requires ESAPI
-configuration to select the implementation.
-
-The ESAPI dependency remains a compile dependency because its `Encoder` type is
-part of the adapter's public API. Its transitive dependencies are therefore also
-available to applications. The unreleased 1.5 POM additionally declares
-`httpclient5:5.6.4`, `httpcore5:5.4.4`, and `httpcore5-h2:5.4.4` as direct compile
-dependencies. These stable versions replace AntiSamy 1.7.8's older HTTP
-transitives in a standalone Maven consumer. Dependency management aligns the
-adapter's own graph for convergence; management alone did not carry those
-versions into a consumer's graph. Applications should scan their complete
-resolved graph because their own dependencies or BOMs can change resolution.
-
-The [ESAPI 2.7.0.0 release][esapi-release] addresses CVE-2025-5878 and updates
-transitive dependencies for CVE-2025-48976 and CVE-2025-48734. Its upstream POM
-intentionally depends on the milestone `commons-collections4` 4.5.0-M2; this
-adapter retains that selection. The targeted HTTP overrides do not change the
-ESAPI or AntiSamy release.
-
-### Dependency security triage
-
-The resolved dependency submissions and [Dependabot alerts][dependency-alerts]
-are the ongoing inventory, including transitive runtime/test dependencies and
-build plugins. Review the actual version, path and execution scope of each new
-alert; the fact that ESAPI introduces a dependency is not a dismissal reason.
-As checked on 2026-09-27, ESAPI 2.7.0.0 remained the newest stable upstream
-release (2.7.0.1-RC1 was a prerelease). The resolved compile graph still has two
-findings, both introduced through `encoder-esapi -> esapi:2.7.0.0`:
-
-- `commons-configuration:commons-configuration:1.10` (compile):
- [GHSA-pvp8-3xj6-8c6x][]. The trigger is loading untrusted configuration or
- attacker-controlled usage patterns, which can consume excessive resources.
- Delegated ESAPI calls can initialize reference configuration; keep it trusted.
- There is no patched 1.x release.
-- `commons-lang:commons-lang:2.6` (compile): [GHSA-j288-q9x7-2f5v][]. The
- trigger is a very long attacker-controlled class name passed to
- `ClassUtils.getClass`, which can cause uncontrolled recursion. There is no
- patched 2.x release.
-
-The adapter's Java Encoder-backed methods perform string encoding without
-these configuration or class-lookup operations. Delegated methods and
-applications using ESAPI's other features require their own reachability
-assessment. Commons Configuration 2.x and Commons Lang 3.x use different
-coordinates/APIs and cannot silently replace these legacy dependencies.
-
-The previous HTTP findings are absent from the 1.5 consumer compile graph:
-`httpclient5:5.6.4` is beyond the patch for [GHSA-hjcp-jmpx-g3qm][], and
-`httpcore5:5.4.4` / `httpcore5-h2:5.4.4` are beyond the patches for
-[GHSA-hf6x-8p5f-cgmf][] / [GHSA-v3jc-474w-2wm6][]. These advisories concern
-classic HTTP response decoding and connection release, HTTP/1 header parsing,
-and HTTP/2 HPACK decoding respectively. They remain relevant to older adapter
-releases or applications that independently resolve affected HTTP versions.
-
-Residual disposition, owner: the OWASP Java Encoder maintainer team retains
-the two Commons findings for upstream/application assessment, without
-suppression. Recheck by **2026-12-26** (90 days from this review) or at the next
-stable ESAPI release, whichever comes first. Review any new upstream fix for
-API/runtime compatibility, dependency convergence, adapter tests, and packaged
-consumers before changing the graph. Record the advisory, resolved path and
-scope, trigger, evidence, owner, and recheck date for any application exception.
-Do not close an alert solely because it is transitive or adapter tests pass.
-
-ESAPI 2.7 disables `encodeForSQL` by default. The adapter preserves that safer
-behavior; use parameterized queries instead of enabling the legacy method.
-
-Every supported ESAPI JAR derives the automatic JPMS module name `esapi` from
-its filename. Keep the original `esapi-.jar` filename when placing it
-on the module path. This stable identity is the one used by the adapter's JPMS
-dependency declaration.
-
-[esapi-security]: https://github.com/ESAPI/esapi-java-legacy/security
-[dependency-alerts]: https://github.com/OWASP/owasp-java-encoder/security/dependabot
-[esapi-latest]: https://github.com/ESAPI/esapi-java-legacy/releases/latest
-[esapi-release]: https://github.com/ESAPI/esapi-java-legacy/releases/tag/esapi-2.7.0.0
-[esapi-url-reference]: https://github.com/ESAPI/esapi-java-legacy/blob/esapi-2.7.0.0/src/main/java/org/owasp/esapi/reference/DefaultEncoder.java#L506-L516
-[encoder-release]: https://github.com/OWASP/owasp-java-encoder/releases/tag/v1.4.1
-[encoder-verification]: ../releases/1.4.1.md#verification
-[encoder-advisories]: ../releases/1.4.1.md#security-fixes
-[GHSA-pvp8-3xj6-8c6x]: https://github.com/advisories/GHSA-pvp8-3xj6-8c6x
-[GHSA-j288-q9x7-2f5v]: https://github.com/advisories/GHSA-j288-q9x7-2f5v
-[GHSA-hjcp-jmpx-g3qm]: https://github.com/advisories/GHSA-hjcp-jmpx-g3qm
-[GHSA-hf6x-8p5f-cgmf]: https://github.com/advisories/GHSA-hf6x-8p5f-cgmf
-[GHSA-v3jc-474w-2wm6]: https://github.com/advisories/GHSA-v3jc-474w-2wm6
diff --git a/esapi/pom.xml b/esapi/pom.xml
deleted file mode 100644
index 6f6cf75..0000000
--- a/esapi/pom.xml
+++ /dev/null
@@ -1,187 +0,0 @@
-
-
-
-
- 4.0.0
-
-
- org.owasp.encoder
- encoder-parent
- 1.5.0-SNAPSHOT
-
-
- encoder-esapi
- jar
- https://owasp.org/projects/java-encoder
-
- ESAPI Thunk
-
- The OWASP Encoders ESAPI Thunk provides an easy way to plugin the Encoder
- Projects API into an implementation of ESAPI.
-
-
-
- 0.964
- 1.000
-
- 2.7.0.0
- org.owasp.encoder.esapi
- org.owasp.encoder.esapi
-
-
- org.owasp.encoder;version="[1.4.1,2)",
- *
-
-
-
-
-
-
-
- org.apache.httpcomponents.client5
- httpclient5
- 5.6.4
-
-
- org.apache.httpcomponents.core5
- httpcore5
- 5.4.4
-
-
- org.apache.httpcomponents.core5
- httpcore5-h2
- 5.4.4
-
-
-
-
-
-
- org.owasp.encoder
- encoder
- ${project.parent.version}
-
-
- org.owasp.esapi
- esapi
- ${esapi.version}
-
-
-
- org.apache.httpcomponents.client5
- httpclient5
-
-
-
- org.slf4j
- slf4j-api
-
-
-
-
- org.apache.httpcomponents.core5
- httpcore5
-
-
- org.apache.httpcomponents.core5
- httpcore5-h2
-
-
-
- org.jsoup
- jsoup
- 1.23.2
- test
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
-
- true
-
-
-
- compile-module-path-test-support
- test-compile
-
- testCompile
-
-
-
- ${project.basedir}/../src/test-support/java
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-failsafe-plugin
-
-
- ${project.build.directory}/${project.build.finalName}.jar
- ${maven.multiModuleProjectDirectory}/core/target/encoder-${project.version}.jar
- org.owasp.esapi.Encoder
- true
- owasp.encoder.esapi.consumer/org.owasp.encoder.consumer.EsapiConsumer
- ${project.basedir}/src/test/modules/owasp.encoder.esapi.consumer
-
-
-
-
- module-path-consumer
-
- integration-test
- verify
-
-
-
-
-
-
-
diff --git a/esapi/src/main/java/org/owasp/encoder/esapi/ESAPIEncoder.java b/esapi/src/main/java/org/owasp/encoder/esapi/ESAPIEncoder.java
deleted file mode 100644
index 61294f1..0000000
--- a/esapi/src/main/java/org/owasp/encoder/esapi/ESAPIEncoder.java
+++ /dev/null
@@ -1,353 +0,0 @@
-// Copyright (c) 2012 Jeff Ichnowski
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions
-// are met:
-//
-// * Redistributions of source code must retain the above
-// copyright notice, this list of conditions and the following
-// disclaimer.
-//
-// * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following
-// disclaimer in the documentation and/or other materials
-// provided with the distribution.
-//
-// * Neither the name of the OWASP nor the names of its
-// contributors may be used to endorse or promote products
-// derived from this software without specific prior written
-// permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
-// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
-// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
-// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-// OF THE POSSIBILITY OF SUCH DAMAGE.
-
-package org.owasp.encoder.esapi;
-
-import java.io.IOException;
-import java.net.URI;
-import org.owasp.encoder.Encode;
-import org.owasp.esapi.Encoder;
-import org.owasp.esapi.codecs.Codec;
-import org.owasp.esapi.errors.EncodingException;
-import org.owasp.esapi.reference.DefaultEncoder;
-
-/**
- * ESAPIEncoder is a singleton implementation of the ESAPI Encoder API. It
- * is meant to allow quick and easy drop-in replacement of the default
- * encoder included with the ESAPI library, as the Encoder libraries are
- * faster and use less memory thus cause fewer garbage collections.
- *
- *
Please note that the OWASP Java Encoders does not implement all
- * the encodings of the ESAPI Encoder API. In such situations this
- * implementation will fallback onto the default reference implementation
- * included with ESAPI. Thus you should see the performance benefit from
- * the methods included in the Encoder, but still maintain compatibility
- * with the delegated methods from ESAPI Encoder. The methods implemented
- * here have the contextual contracts described below.
- *
- *
The adapter's {@code encodeForCSS} encodes only quoted CSS strings, and
- * {@code encodeForJavaScript} encodes single- or double-quoted JavaScript
- * strings and literal text in ordinary (untagged) template literals, not JSON,
- * tagged templates (including {@code String.raw}), template expression bodies,
- * or script URLs. It escapes
- * DEL/C1 controls and unpaired UTF-16 surrogates while preserving valid pairs.
- * Neither method encodes arbitrary unquoted CSS or JavaScript code.
- *
- *
{@code encodeForHTMLAttribute} encodes quoted HTML text attributes using
- * {@link Encode#forHtmlAttribute(String)}. It does not make event-handler code
- * or an untrusted URL safe. For a URL-valued attribute, validate the complete
- * URL against application rules, including allowed schemes, then encode it for
- * the enclosing HTML attribute.
- *
- *
Starting with 1.5, {@code encodeForURL} encodes one raw URL component using
- * {@link Encode#forUriComponent(String)}. It percent-encodes UTF-8 bytes,
- * including delimiters such as {@code & = / ? # +}, and uses {@code %20}
- * for spaces. This intentionally differs from ESAPI's reference form encoding,
- * which uses {@code +} for spaces and a configurable character encoding. The
- * adapter retains its historical {@code "null"} result for {@code null} input
- * and replaces unpaired UTF-16 surrogates with {@code -}. Already percent-encoded
- * input is encoded again. It neither validates a complete URL nor reads ESAPI
- * configuration. Assemble the URL from trusted structure and encoded raw
- * components, validate it for its intended use, then encode for the enclosing
- * output context. For form encoding specifically, use
- * {@link java.net.URLEncoder} with an explicit character encoding.
- *
- *
The following methods delegate to ESAPI. Most are outside the scope of
- * contextual output encoding; JSON encoding retains the reference behavior
- * for compatibility, including its {@code null} result for {@code null} input:
JSON encoding and decoding:
- * {@link org.owasp.esapi.Encoder#encodeForJSON(String)},
- * {@link org.owasp.esapi.Encoder#decodeFromJSON(String)}.
- * For the core library's JSON string context, use {@link Encode#forJson(String)}
- * directly; its escaping and null contract differ from ESAPI's.
(Please note that with sufficient feedback from the user base, the above
- * mentioned methods may be implemented in future releases of the OWASP
- * Java Encoders, if/when that happens, this shim class will be updated to
- * call out to the new methods.)
- *
- *
You may notice that this class does not actually implement Encoder
- * itself. Instead it simply provides a {@link #getInstance()} method that
- * does. This allows the implementation details maximum flexibility by not
- * creating a any public API that would restrict changes later
- *
- * @author jeffi
- */
-public final class ESAPIEncoder {
-
- /** No instances. */
- private ESAPIEncoder() {}
-
- /**
- * Returns an instance of the Encoder. This method is the only supported
- * mechanism by which an ESAPIEncoder instance should be obtained. The
- * returned implementation is guaranteed to be thread-safe for the methods
- * that the OWASP Java Encoders implement (see class documentation).
- * Though not a requirement of the ESAPI Encoder API, the returned value
- * is also serializable.
- * Obtaining the instance and using OWASP Java Encoder-backed methods does
- * not load ESAPI configuration. Delegated methods resolve ESAPI's reference
- * encoder when called and require its configuration to be available.
- *
- * @return An encoder implementation that uses the OWASP Java Encoders
- * for most of the common encoding methods.
- */
- public static Encoder getInstance() {
- return Impl.INSTANCE;
- }
-
- /**
- * This is the private singleton that implements the ESAPI Encoder shim.
- * It is implemented as a single-value enum to get all the "free" singleton
- * properties associated with enums, including serialization and thread-safe
- * initialization. Initializing this enum does not initialize ESAPI's
- * reference encoder.
- *
- *
The implementation is intentionally private to avoid any API baggage.
- * The instance should be obtained using
- * {@link org.owasp.encoder.esapi.ESAPIEncoder#getInstance()}.
- */
- private enum Impl implements Encoder {
- /**
- * The singleton instance.
- */
- INSTANCE;
-
- /**
- * Resolves ESAPI's reference encoder for each delegated call. ESAPI
- * caches the successful singleton itself. Keeping this call out of
- * class initialization allows a failed configuration load to be retried.
- */
- private static Encoder reference() {
- return DefaultEncoder.getInstance();
- }
-
- /** {@inheritDoc} */
- @Override
- public String canonicalize(String s) {
- return reference().canonicalize(s);
- }
-
- /** {@inheritDoc} */
- @Override
- public String canonicalize(String s, boolean strict) {
- return reference().canonicalize(s, strict);
- }
-
- /** {@inheritDoc} */
- @Override
- public String canonicalize(String s, boolean restrictMultiple, boolean restrictMixed) {
- return reference().canonicalize(s, restrictMultiple, restrictMixed);
- }
-
- /** {@inheritDoc} */
- @Override
- public String getCanonicalizedURI(URI dirtyUri) {
- return reference().getCanonicalizedURI(dirtyUri);
- }
-
- /**
- * Encodes a quoted CSS string using {@link Encode#forCssString(String)}.
- * This is not an encoder for arbitrary CSS expressions or property values.
- */
- @Override
- public String encodeForCSS(String s) {
- return Encode.forCssString(s);
- }
-
- /** {@inheritDoc} */
- @Override
- public String encodeForHTML(String s) {
- return Encode.forHtml(s);
- }
-
- /** {@inheritDoc} */
- @Override
- public String decodeForHTML(String s) {
- return reference().decodeForHTML(s);
- }
-
- /**
- * Encodes a quoted HTML text attribute, not an unquoted attribute,
- * event-handler program, or unvalidated URL. See
- * {@link Encode#forHtmlAttribute(String)} for the enclosing-context rules.
- */
- @Override
- public String encodeForHTMLAttribute(String s) {
- return Encode.forHtmlAttribute(s);
- }
-
- /**
- * Encodes a single- or double-quoted JavaScript string or literal text in an
- * ordinary (untagged) template literal using {@link Encode#forJavaScript(String)}.
- * Not for JSON, tagged templates (including {@code String.raw}), template
- * expression bodies, unquoted code, or script URLs.
- * DEL/C1 controls and unpaired UTF-16 surrogates are escaped; valid pairs
- * remain unescaped.
- */
- @Override
- public String encodeForJavaScript(String s) {
- return Encode.forJavaScript(s);
- }
-
- /** {@inheritDoc} */
- @Override
- public String encodeForVBScript(String s) {
- return reference().encodeForVBScript(s);
- }
-
- /** {@inheritDoc} */
- @Override
- public String encodeForSQL(Codec codec, String s) {
- return reference().encodeForSQL(codec, s);
- }
-
- /** {@inheritDoc} */
- @Override
- public String encodeForOS(Codec codec, String s) {
- return reference().encodeForOS(codec, s);
- }
-
- /** {@inheritDoc} */
- @Override
- public String encodeForLDAP(String s) {
- return reference().encodeForLDAP(s);
- }
-
- /** {@inheritDoc} */
- @Override
- public String encodeForLDAP(String s, boolean b) {
- return reference().encodeForLDAP(s, b);
- }
-
- /** {@inheritDoc} */
- @Override
- public String encodeForDN(String s) {
- return reference().encodeForDN(s);
- }
-
- /** {@inheritDoc} */
- @Override
- public String encodeForXPath(String s) {
- return reference().encodeForXPath(s);
- }
-
- /** {@inheritDoc} */
- @Override
- public String encodeForXML(String s) {
- return Encode.forXml(s);
- }
-
- /** {@inheritDoc} */
- @Override
- public String encodeForXMLAttribute(String s) {
- return Encode.forXmlAttribute(s);
- }
-
- /**
- * Encodes a raw URL component as UTF-8 with spaces as {@code %20}, using
- * {@link Encode#forUriComponent(String)}. Retains the adapter's
- * {@code "null"} result for null and {@code -} for unpaired surrogates.
- * The checked exception remains in the ESAPI interface contract;
- * this implementation does not depend on a configurable charset.
- */
- @Override
- public String encodeForURL(String s) throws EncodingException {
- return Encode.forUriComponent(s);
- }
-
- /** {@inheritDoc} */
- @Override
- public String decodeFromURL(String s) throws EncodingException {
- return reference().decodeFromURL(s);
- }
-
- /** {@inheritDoc} */
- @Override
- public String encodeForBase64(byte[] bytes, boolean wrap) {
- return reference().encodeForBase64(bytes, wrap);
- }
-
- /** {@inheritDoc} */
- @Override
- public byte[] decodeFromBase64(String s) throws IOException {
- return reference().decodeFromBase64(s);
- }
-
- /**
- * Delegates JSON string encoding to the ESAPI reference encoder.
- */
- @Override
- public String encodeForJSON(String s) {
- return reference().encodeForJSON(s);
- }
-
- /**
- * Delegates JSON string decoding to the ESAPI reference encoder.
- */
- @Override
- public String decodeFromJSON(String s) {
- return reference().decodeFromJSON(s);
- }
-
- }
-}
diff --git a/esapi/src/main/java9/module-info.java b/esapi/src/main/java9/module-info.java
deleted file mode 100644
index 45cf4c6..0000000
--- a/esapi/src/main/java9/module-info.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (c) 2024 Jeremy Long
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions
-// are met:
-//
-// * Redistributions of source code must retain the above
-// copyright notice, this list of conditions and the following
-// disclaimer.
-//
-// * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following
-// disclaimer in the documentation and/or other materials
-// provided with the distribution.
-//
-// * Neither the name of the OWASP nor the names of its
-// contributors may be used to endorse or promote products
-// derived from this software without specific prior written
-// permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
-// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
-// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
-// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-// OF THE POSSIBILITY OF SUCH DAMAGE.
-
-module owasp.encoder.esapi {
- requires transitive esapi;
- requires owasp.encoder;
-
- exports org.owasp.encoder.esapi;
-}
diff --git a/esapi/src/main/resources/META-INF/LICENSE b/esapi/src/main/resources/META-INF/LICENSE
deleted file mode 100644
index f66c375..0000000
--- a/esapi/src/main/resources/META-INF/LICENSE
+++ /dev/null
@@ -1,33 +0,0 @@
-Copyright (c) 2015 Jeff Ichnowski
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-
- * Redistributions of source code must retain the above
- copyright notice, this list of conditions and the following
- disclaimer.
-
- * Redistributions in binary form must reproduce the above
- copyright notice, this list of conditions and the following
- disclaimer in the documentation and/or other materials
- provided with the distribution.
-
- * Neither the name of the OWASP nor the names of its
- contributors may be used to endorse or promote products
- derived from this software without specific prior written
- permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
-COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
-INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
-STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/esapi/src/site/site.xml b/esapi/src/site/site.xml
deleted file mode 100644
index 743ef44..0000000
--- a/esapi/src/site/site.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/esapi/src/test/java/org/owasp/encoder/esapi/ESAPIContextTest.java b/esapi/src/test/java/org/owasp/encoder/esapi/ESAPIContextTest.java
deleted file mode 100644
index e1ca466..0000000
--- a/esapi/src/test/java/org/owasp/encoder/esapi/ESAPIContextTest.java
+++ /dev/null
@@ -1,202 +0,0 @@
-// Copyright (c) 2026 OWASP
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions
-// are met:
-//
-// * Redistributions of source code must retain the above
-// copyright notice, this list of conditions and the following
-// disclaimer.
-//
-// * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following
-// disclaimer in the documentation and/or other materials
-// provided with the distribution.
-//
-// * Neither the name of the OWASP nor the names of its
-// contributors may be used to endorse or promote products
-// derived from this software without specific prior written
-// permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
-// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
-// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
-// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-// OF THE POSSIBILITY OF SUCH DAMAGE.
-
-package org.owasp.encoder.esapi;
-
-import java.net.URI;
-import java.net.URLDecoder;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import junit.framework.TestCase;
-import org.jsoup.Jsoup;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
-import org.owasp.esapi.ESAPI;
-import org.owasp.esapi.Encoder;
-import org.owasp.esapi.errors.EncodingException;
-import org.owasp.esapi.reference.DefaultEncoder;
-
-/** Context and value contracts, also exercised by the stable ESAPI matrix. */
-public class ESAPIContextTest extends TestCase {
- private final Encoder encoder = ESAPIEncoder.getInstance();
-
- public void testUrlComponentEncodingAndReferenceFormDifferences() throws Exception {
- assertEquals("UTF-8", ESAPI.securityConfiguration().getCharacterEncoding());
- Encoder reference = DefaultEncoder.getInstance();
- // Input, adapter component encoding, ESAPI reference form encoding.
- String[][] cases = {
- {"", "", ""},
- {"abcABC123-._", "abcABC123-._", "abcABC123-._"},
- {"a b+c", "a%20b%2Bc", "a+b%2Bc"},
- {"~*", "~%2A", "%7E*"},
- {"&=/?#", "%26%3D%2F%3F%23", "%26%3D%2F%3F%23"},
- {":[]@!$'(),;", "%3A%5B%5D%40%21%24%27%28%29%2C%3B",
- "%3A%5B%5D%40%21%24%27%28%29%2C%3B"},
- {"%20%zz%", "%2520%25zz%25", "%2520%25zz%25"},
- {"\"<>\\`{}", "%22%3C%3E%5C%60%7B%7D", "%22%3C%3E%5C%60%7B%7D"},
- {"\u00e9\u03a9\ud83d\ude00", "%C3%A9%CE%A9%F0%9F%98%80",
- "%C3%A9%CE%A9%F0%9F%98%80"},
- {"\u0000\r\n\t\u007f", "%00%0D%0A%09%7F", "%00%0D%0A%09%7F"}
- };
- for (String[] example : cases) {
- assertEquals(example[0], example[1], encoder.encodeForURL(example[0]));
- assertEquals(example[0], example[2], reference.encodeForURL(example[0]));
- assertEquals(example[0], URLDecoder.decode(example[1], "UTF-8"));
- assertEquals(example[0], URLDecoder.decode(example[2], "UTF-8"));
- }
- }
-
- public void testUrlRetainsAdapterNullAndMalformedUtf16Policy() throws Exception {
- Encoder reference = DefaultEncoder.getInstance();
- assertEquals("null", encoder.encodeForURL(null));
- assertNull(reference.encodeForURL(null));
- String[][] cases = {
- {"\ud800", "-", "%3F"},
- {"\udfff", "-", "%3F"},
- {"a\ud800z", "a-z", "a%3Fz"},
- {"\udc00\ud800", "--", "%3F%3F"},
- {"\ud800\ud800\udc00", "-%F0%90%80%80", "%3F%F0%90%80%80"}
- };
- for (String[] example : cases) {
- assertEquals(example[1], encoder.encodeForURL(example[0]));
- assertEquals(example[2], reference.encodeForURL(example[0]));
- }
- }
-
- public void testUrlEncodingCannotAddQueryParametersOrFragments() throws Exception {
- String input = "a b+c&admin=true/../?other=value#fragment\u03a9\ud83d\ude00";
- String encoded = encoder.encodeForURL(input);
- URI url = new URI("https://example.test/search?q=" + encoded + "&page=1");
- assertEquals("https", url.getScheme());
- assertEquals("example.test", url.getHost());
- assertEquals("/search", url.getRawPath());
- assertNull(url.getRawFragment());
- String[] query = url.getRawQuery().split("&", -1);
- assertEquals(2, query.length);
- assertEquals("page=1", query[1]);
- String[] parameter = query[0].split("=", -1);
- assertEquals(2, parameter.length);
- assertEquals("q", parameter[0]);
- assertEquals(input, URLDecoder.decode(parameter[1], "UTF-8"));
-
- URI path = new URI("https://example.test/items/" + encoded + "/detail");
- assertNull(path.getRawQuery());
- assertNull(path.getRawFragment());
- assertEquals(4, path.getRawPath().split("/", -1).length);
- assertEquals(input, URLDecoder.decode(path.getRawPath().split("/", -1)[2], "UTF-8"));
- }
-
- public void testUrlKeepsCheckedExceptionInterface() throws Exception {
- assertEquals(Arrays.asList(EncodingException.class), Arrays.asList(
- Encoder.class.getMethod("encodeForURL", String.class).getExceptionTypes()));
- assertEquals(Arrays.asList(EncodingException.class), Arrays.asList(
- encoder.getClass().getMethod("encodeForURL", String.class).getExceptionTypes()));
- }
-
- public void testQuotedHtmlAttributesPreserveDataAndCannotAddMarkup() {
- String[] inputs = {"", "'\" autofocus onfocus=alert(1) x='\"", "