com.googlecode.owasp-java-html-sanitizer
diff --git a/java8-shim/src/main/java/org/owasp/shim/Java8Shim.java b/java8-shim/src/main/java/org/owasp/shim/Java8Shim.java
index c949a052..181285ed 100644
--- a/java8-shim/src/main/java/org/owasp/shim/Java8Shim.java
+++ b/java8-shim/src/main/java/org/owasp/shim/Java8Shim.java
@@ -19,7 +19,9 @@ public abstract class Java8Shim {
Object _instance;
try {
try {
- // This is compiled with -release 1.10 in a separate project.
+ // java10-shim compiles this for Java 10, and the sanitizer jar
+ // carries it in META-INF/versions/10/, so only Java 10 and
+ // later find it.
_instance = Class.forName("org.owasp.shim.ForJava10AndLater").newInstance();
} catch (Throwable e) {
// This is co-located with this project and is a fall-back.
diff --git a/owasp-java-html-sanitizer/README.md b/owasp-java-html-sanitizer/README.md
index 75258cbb..f05f9ef3 100644
--- a/owasp-java-html-sanitizer/README.md
+++ b/owasp-java-html-sanitizer/README.md
@@ -24,6 +24,11 @@ rendering to produce sanitized HTML. See the
against the packaged JAR during the `integration-test` phase, included in
`verify`. It checks the descriptor and confirms that the bundled shim
package remains encapsulated.
+* The [Java 8 consumer](src/it/java8-consumer/) is compiled for Java 8 and run
+ against the packaged JAR on the class path in the same phase, and on a
+ Java 8 runtime in CI. It fails if any class in the JAR is newer than a
+ Java 8 build allows, or if the sanitizer does not pick the shim meant for
+ the running JDK.
`HtmlElementTablesCanned.java` is generated from browser observations. Do
not edit it directly; update the experiment or data in
diff --git a/owasp-java-html-sanitizer/pom.xml b/owasp-java-html-sanitizer/pom.xml
index c63f703d..706b7179 100644
--- a/owasp-java-html-sanitizer/pom.xml
+++ b/owasp-java-html-sanitizer/pom.xml
@@ -141,12 +141,16 @@
wrong directory. It is exactly where the JDK expects it,
so drop that one warning and no other. -->
<_fixupmessages>Classes found in the wrong directory*module-info*;is:=ignore
-
+
true
+
+ com.googlecode.owasp-java-html-sanitizer:java10-shim
+
+ META-INF/versions/**
+
+
@@ -268,6 +287,50 @@
+
+
+ compile-java8-consumer
+ integration-test
+
+ exec
+
+
+ ${java.home}/bin/javac
+
+ --release
+ 8
+
+ -Xlint:-options
+ -d
+ ${project.build.directory}/java8-consumer
+ -classpath
+ ${project.build.directory}/${project.build.finalName}.jar
+ ${project.basedir}/src/it/java8-consumer/org/owasp/html/java8check/Java8ConsumerCheck.java
+
+
+
+
+ run-java8-consumer
+ integration-test
+
+ exec
+
+
+ ${java.home}/bin/java
+
+ -classpath
+ ${project.build.directory}/${project.build.finalName}.jar${path.separator}${project.build.directory}/java8-consumer
+ org.owasp.html.java8check.Java8ConsumerCheck
+ ${project.build.directory}/${project.build.finalName}.jar
+
+
+
diff --git a/owasp-java-html-sanitizer/src/it/java8-consumer/org/owasp/html/java8check/Java8ConsumerCheck.java b/owasp-java-html-sanitizer/src/it/java8-consumer/org/owasp/html/java8check/Java8ConsumerCheck.java
new file mode 100644
index 00000000..2cbee7ef
--- /dev/null
+++ b/owasp-java-html-sanitizer/src/it/java8-consumer/org/owasp/html/java8check/Java8ConsumerCheck.java
@@ -0,0 +1,134 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-2-Clause
+
+package org.owasp.html.java8check;
+
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import org.owasp.html.Sanitizers;
+import org.owasp.shim.Java8Shim;
+
+/**
+ * Checks, from the class path, that the packaged jar is usable by a consumer
+ * that builds for or runs on Java 8. See issue #515.
+ *
+ * Java 8 builds commonly fail on any dependency that holds a class
+ * compiled for a later release, with the Maven Enforcer rule
+ * {@code enforceBytecodeVersion} at a {@code maxJdkVersion} of 1.8. That
+ * rule allows release N under {@code META-INF/versions/N/} of a
+ * multi-release jar, where only Java N and later look, and nothing newer
+ * than Java 8 anywhere else. So this checks every class in the jar against
+ * the same bound, then checks that the sanitizer picks the shim meant for
+ * the runtime it is on, which is how a mistake in the versioned directory or
+ * the {@code Multi-Release} manifest header would show.
+ *
+ *
The build runs this at the integration-test phase on the JDK running
+ * the build, and CI also runs it on a Java 8 runtime, the one place the
+ * Java 8 fallback shim is ever loaded. Any failure exits non-zero. It is
+ * compiled for Java 8, so keep to Java 8 APIs here.
+ */
+public final class Java8ConsumerCheck {
+
+ private Java8ConsumerCheck() {}
+
+ /** A versioned entry of a multi-release jar, capturing its release. */
+ private static final Pattern VERSIONED =
+ Pattern.compile("^META-INF/versions/([0-9]+)/");
+
+ /** Release N compiles to class file major version N + 44. */
+ private static final int MAJOR_VERSION_OFFSET = 44;
+
+ public static void main(String[] args) throws Exception {
+ check(args.length == 1, "usage: Java8ConsumerCheck ");
+ File jar = new File(args[0]).getCanonicalFile();
+
+ // What a Java 8 build checks: nothing outside the versioned directories
+ // is newer than Java 8, and nothing inside one is newer than its release.
+ List tooNew = new ArrayList<>();
+ int classCount = 0;
+ try (ZipFile zip = new ZipFile(jar)) {
+ for (Enumeration extends ZipEntry> entries = zip.entries();
+ entries.hasMoreElements();) {
+ ZipEntry entry = entries.nextElement();
+ String name = entry.getName();
+ if (entry.isDirectory() || !name.endsWith(".class")) {
+ continue;
+ }
+ ++classCount;
+ Matcher versioned = VERSIONED.matcher(name);
+ boolean isVersioned = versioned.find();
+ int release = isVersioned ? Integer.parseInt(versioned.group(1)) : 8;
+ int major = majorVersion(zip, entry);
+ if (major > release + MAJOR_VERSION_OFFSET) {
+ tooNew.add(name + " is compiled for Java "
+ + (major - MAJOR_VERSION_OFFSET) + " but "
+ + (isVersioned ? "sits in the directory for Java " + release
+ : "is outside META-INF/versions/"));
+ }
+ }
+ }
+ check(classCount != 0, "no classes in " + jar);
+ check(tooNew.isEmpty(), "a Java 8 build rejects this jar: " + tooNew);
+
+ // Java 10 and later read the versioned directory and find the shim that
+ // uses their collection factories; Java 8 and 9 never see it and fall
+ // back.
+ int runtime = runtimeRelease();
+ String expected = runtime >= 10
+ ? "org.owasp.shim.ForJava10AndLater" : "org.owasp.shim.ForJava8";
+ Class> shimClass = Java8Shim.j8().getClass();
+ String shim = shimClass.getName();
+ check(expected.equals(shim),
+ "on Java " + runtime + " the sanitizer loaded " + shim
+ + " rather than " + expected);
+
+ // Those classes must come from the jar under test, not from a stray shim
+ // or class directory earlier on the class path.
+ for (Class> c : new Class>[] {
+ Sanitizers.class, Java8Shim.class, shimClass }) {
+ File source = new File(
+ c.getProtectionDomain().getCodeSource().getLocation().toURI())
+ .getCanonicalFile();
+ check(jar.equals(source), c.getName() + " loaded from " + source);
+ }
+
+ String sanitized = Sanitizers.FORMATTING.sanitize(
+ "safe");
+ check("safe".equals(sanitized), "sanitized to " + sanitized);
+
+ System.out.println(classCount + " classes in " + jar.getName()
+ + " are within a Java 8 consumer's bounds; on Java " + runtime
+ + " the sanitizer uses " + shim);
+ }
+
+ private static int majorVersion(ZipFile zip, ZipEntry entry)
+ throws IOException {
+ try (InputStream in = zip.getInputStream(entry)) {
+ DataInputStream data = new DataInputStream(in);
+ check(data.readInt() == 0xCAFEBABE, entry.getName() + " is not a class");
+ data.readUnsignedShort(); // minor version
+ return data.readUnsignedShort();
+ }
+ }
+
+ /** 8 for "1.8", and the number itself from Java 9's "9" on. */
+ private static int runtimeRelease() {
+ String spec = System.getProperty("java.specification.version");
+ return Integer.parseInt(spec.startsWith("1.") ? spec.substring(2) : spec);
+ }
+
+ private static void check(boolean condition, String failure) {
+ if (!condition) {
+ throw new AssertionError(failure);
+ }
+ }
+}
diff --git a/owasp-java-html-sanitizer/src/it/jpms-consumer/org/owasp/html/jpmscheck/JpmsConsumerCheck.java b/owasp-java-html-sanitizer/src/it/jpms-consumer/org/owasp/html/jpmscheck/JpmsConsumerCheck.java
index 312e392e..193377c4 100644
--- a/owasp-java-html-sanitizer/src/it/jpms-consumer/org/owasp/html/jpmscheck/JpmsConsumerCheck.java
+++ b/owasp-java-html-sanitizer/src/it/jpms-consumer/org/owasp/html/jpmscheck/JpmsConsumerCheck.java
@@ -99,6 +99,24 @@ public static void main(String[] args) {
throw new AssertionError("the shim did not fail in the expected way", e);
}
+ // On Java 10 and later the module also holds the shim compiled for them,
+ // which the jar carries only in META-INF/versions/10/ so that Java 8
+ // builds accept it. It is as encapsulated as the rest of its package,
+ // so load it, uninitialized, through the module's class loader. See
+ // issue #515.
+ if (Runtime.version().major() >= 10) {
+ Class> shim;
+ try {
+ shim = Class.forName(
+ "org.owasp.shim.ForJava10AndLater", false, module.getClassLoader());
+ } catch (ClassNotFoundException e) {
+ throw new AssertionError("the module has no ForJava10AndLater on Java "
+ + Runtime.version().major(), e);
+ }
+ check(module.equals(shim.getModule()),
+ "ForJava10AndLater is in " + shim.getModule());
+ }
+
// And the sanitizer works from the module path, which drives the shim
// loader inside the module.
String sanitized = Sanitizers.FORMATTING.sanitize(