diff --git a/README.md b/README.md
index 801a876..faf4cb9 100644
--- a/README.md
+++ b/README.md
@@ -137,6 +137,26 @@ The ESAPI adapter's fixed dependency and tested compatibility policy are
documented in [esapi/README.md](esapi/README.md).
+OSGi Bundles
+------------
+
+| JAR | Bundle-SymbolicName | Export-Package | Imports `org.owasp.encoder` | Imports API packages |
+|---------------------|---------------------------------|--------------------------|-----------------------------|-------------------------------------------------------------|
+| 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.
+
TagLib
--------------------
@@ -206,6 +226,7 @@ Development builds use `1.5.0-SNAPSHOT`; this is not a published release.
* 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.
* 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.
* 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](#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: `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.
diff --git a/compatibility/README.md b/compatibility/README.md
index 1d482cc..7062f07 100644
--- a/compatibility/README.md
+++ b/compatibility/README.md
@@ -60,7 +60,11 @@ unrelated split packages among legacy dependency JARs.
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.
+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
+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.
Encoder code is absent from the host classpath. This tests the encoder bundles'
imports, resolution, and execution; it does not test independently installed
vendor API bundles or a full servlet container. Legacy Felix URL handlers are
@@ -70,7 +74,7 @@ disabled because this fixture does not use them.
Preparation asserts all four artifacts' automatic/explicit module names,
descriptor requirements (including transitive API readability), exports, OSGi
-identities, imports and export versions, absence of execution-environment
+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
referenced packaged classes, and allowed published runtime dependencies. Base
classes must stay within each artifact's own package; test classes and embedded
diff --git a/compatibility/consumers.py b/compatibility/consumers.py
index 09a9e7d..1bc5752 100644
--- a/compatibility/consumers.py
+++ b/compatibility/consumers.py
@@ -26,6 +26,20 @@
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']}
+# 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).
+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.
+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},
+}
def run(*args, **kwargs):
@@ -65,9 +79,13 @@ def metadata(kind, jar, core):
assert [entry.split(';')[0] for entry in exports] == [package], exports
assert ';version="' + '.'.join(attrs['Bundle-Version'].split('.')[:3]) + '"' in exports[0], exports
imports = clauses(attrs.get('Import-Package', ''))
- expected_imports = set(HOST_PACKAGES[kind]) - {'javax.servlet.jsp.el', 'javax.el', 'jakarta.servlet.jsp.el', 'jakarta.el'}
- if kind != 'core': expected_imports.add('org.owasp.encoder')
- assert set(x.split(';')[0] for x in imports) == expected_imports, imports
+ actual_ranges = {}
+ for entry in imports:
+ name, *parameters = entry.split(';')
+ versions = [p.split('=', 1)[1].strip('"') for p in parameters if p.startswith('version=')]
+ assert name not in actual_ranges, ('duplicate import', entry)
+ 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
with zipfile.ZipFile(jar) as archive, zipfile.ZipFile(core) as core_archive:
names = archive.namelist()
@@ -161,7 +179,7 @@ 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'):
+ for kind in ('jsp', 'jakarta', 'esapi', '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.9.0:copy-dependencies',
@@ -225,12 +243,21 @@ def consume(args):
run(java, *config, '-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 '')
+ for p in HOST_PACKAGES[kind])
+ legacy_core = sorted((out / 'dependencies' / 'legacy-core').glob('encoder-*.jar'))
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',
- storage, ','.join(HOST_PACKAGES[kind]), 'consumer.' + main,
+ 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',
+ storage, host, '--expect-unresolved', legacy_core[0], jars[kind])
print('PASS Java', args.runtime, kind, flush=True)
diff --git a/compatibility/dependencies/legacy-core.xml b/compatibility/dependencies/legacy-core.xml
new file mode 100644
index 0000000..623a20b
--- /dev/null
+++ b/compatibility/dependencies/legacy-core.xml
@@ -0,0 +1,3 @@
+4.0.0org.owasp.encoder.testsconsumer-legacy-core1
+ org.owasp.encoderencoder1.4.0
+
diff --git a/compatibility/src/OsgiConsumer.java b/compatibility/src/OsgiConsumer.java
index 8f4119e..888f8a1 100644
--- a/compatibility/src/OsgiConsumer.java
+++ b/compatibility/src/OsgiConsumer.java
@@ -5,6 +5,7 @@
import java.util.Map;
import org.apache.felix.framework.FrameworkFactory;
import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.launch.Framework;
@@ -22,12 +23,28 @@ public static void main(String[] args) throws Exception {
config.put(Constants.FRAMEWORK_STORAGE, args[0]);
config.put(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, args[1]);
config.put("felix.service.urlhandlers", "false");
+ // "--expect-unresolved" installs an older core and the adapter, which must
+ // then fail to resolve against it instead of wiring (#137).
+ boolean expectUnresolved = "--expect-unresolved".equals(args[2]);
Framework framework = new FrameworkFactory().newFramework(config);
framework.init();
try {
framework.start();
for (int i = 3; i < args.length; ++i) {
Bundle bundle = framework.getBundleContext().installBundle(new File(args[i]).toURI().toString());
+ if (expectUnresolved && i == args.length - 1) {
+ try {
+ bundle.start();
+ } catch (BundleException expected) {
+ String message = String.valueOf(expected.getMessage());
+ if (bundle.getState() == Bundle.ACTIVE || !message.contains("org.owasp.encoder")) {
+ throw new AssertionError("Unexpected failure: " + message, expected);
+ }
+ System.out.println("Rejected as expected: " + message);
+ return;
+ }
+ throw new AssertionError(bundle.getSymbolicName() + " wired to an unsupported core");
+ }
bundle.start();
if (bundle.getState() != Bundle.ACTIVE) throw new AssertionError(bundle);
if (i == args.length - 1) {
diff --git a/compatibility/src/TagConsumer.java b/compatibility/src/TagConsumer.java
index 3658b56..d025110 100644
--- a/compatibility/src/TagConsumer.java
+++ b/compatibility/src/TagConsumer.java
@@ -44,6 +44,7 @@
import javax.servlet.jsp.el.ExpressionEvaluator;
import javax.servlet.jsp.el.VariableResolver;
import org.owasp.encoder.tag.ForHtmlTag;
+import org.owasp.encoder.tag.ForJsonTag;
public final class TagConsumer {
public static void main(String[] args) throws Exception {
@@ -54,6 +55,17 @@ public static void main(String[] args) throws Exception {
tag.setJspContext(new TestJspContext(writer));
tag.doTag();
Checks.encoded(writer.getContentAsString());
+
+ // ForJsonTag calls Encode.forJson, added in 1.5; this proves that linkage.
+ TestJspWriter jsonWriter = new TestJspWriter();
+ ForJsonTag json = new ForJsonTag();
+ json.setValue("'");
+ json.setJspContext(new TestJspContext(jsonWriter));
+ json.doTag();
+ String expected = "\\u003c/script\\u003e'";
+ if (!expected.equals(jsonWriter.getContentAsString())) {
+ throw new AssertionError(jsonWriter.getContentAsString());
+ }
}
/** Minimal JSP context used by tags that only write to {@link #getOut()}. */
diff --git a/compatibility/tests/test_guards.py b/compatibility/tests/test_guards.py
index e545932..83acd34 100644
--- a/compatibility/tests/test_guards.py
+++ b/compatibility/tests/test_guards.py
@@ -93,6 +93,31 @@ def mutate(entries):
entries[name] = entries[name].replace(b'provided', b'test')
self.rejected('jsp', mutate)
+ @staticmethod
+ def header(entries, key, change):
+ """Rewrite one manifest header; bnd folds long lines, so patching bytes is unreliable."""
+ name = 'META-INF/MANIFEST.MF'
+ text = entries[name].decode().replace('\r\n', '\n').replace('\n ', '')
+ lines = [key + ': ' + change(line.split(': ', 1)[1]) if line.startswith(key + ': ') else line
+ for line in text.split('\n')]
+ entries[name] = '\r\n'.join(lines).encode()
+
+ def test_unversioned_core_import(self):
+ self.rejected('jsp', lambda entries: self.header(
+ 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)')))
+
+ def test_widened_jakarta_pages_range(self):
+ self.rejected('jakarta', lambda entries: self.header(
+ entries, 'Import-Package', lambda value: value.replace('[3.0,4)', '[3.0,5)')))
+
+ def test_changed_symbolic_name(self):
+ self.rejected('jakarta', lambda entries: self.header(
+ entries, 'Bundle-SymbolicName', lambda value: 'org.owasp.encoder.jakarta'))
+
if __name__ == '__main__':
unittest.main()
diff --git a/core/pom.xml b/core/pom.xml
index 2efdaee..7893375 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -58,6 +58,7 @@
org.owasp.encoder
+ org.owasp.encoder
diff --git a/esapi/pom.xml b/esapi/pom.xml
index 2e5287f..daa2f32 100644
--- a/esapi/pom.xml
+++ b/esapi/pom.xml
@@ -58,6 +58,15 @@
2.7.0.0
org.owasp.encoder.esapi
+ org.owasp.encoder.esapi
+
+
+ org.owasp.encoder;version="[1.4.1,2)",
+ *
+
diff --git a/jakarta/pom.xml b/jakarta/pom.xml
index b73968f..dba37d1 100644
--- a/jakarta/pom.xml
+++ b/jakarta/pom.xml
@@ -57,6 +57,15 @@
org.owasp.encoder.jakarta
+ org.owasp.encoder.jakarta-jsp
+
+
+ org.owasp.encoder;version="[1.5,2)",
+ jakarta.servlet.jsp;version="[3.0,4)",
+ jakarta.servlet.jsp.tagext;version="[3.0,4)",
+ *
+
diff --git a/jsp/pom.xml b/jsp/pom.xml
index cfc6ebe..8b28403 100644
--- a/jsp/pom.xml
+++ b/jsp/pom.xml
@@ -57,6 +57,15 @@
org.owasp.encoder.jsp
+ org.owasp.encoder.jsp
+
+
+ org.owasp.encoder;version="[1.5,2)",
+ javax.servlet.jsp;version="[2.0,3)",
+ javax.servlet.jsp.tagext;version="[2.0,3)",
+ *
+
diff --git a/pom.xml b/pom.xml
index 08f62b1..c2eace3 100755
--- a/pom.xml
+++ b/pom.xml
@@ -131,6 +131,9 @@
UTF-8
UTF-8
+
+
+ *
@@ -356,6 +359,11 @@
${jigsaw.module.name}
!META-INF.versions.*,*
+
+ ${osgi.symbolic.name}
+
+ ${osgi.import.package}