This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-bcel.git
The following commit(s) were added to refs/heads/master by this push:
new 14890bf2 Repositories cache classes under the input-defined this_class
name; global static Repository/VerifierFactory make the poisoning
ClassLoader-wide (f012).
14890bf2 is described below
commit 14890bf2b9014df25f9b4de86f29b5e917e5656b
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Sep 4 17:06:23 2026 -0400
Repositories cache classes under the input-defined this_class name;
global static Repository/VerifierFactory make the poisoning
ClassLoader-wide (f012).
---
.../bcel/util/AbstractClassPathRepository.java | 6 +++
.../apache/bcel/util/ClassLoaderRepository.java | 6 +++
.../apache/bcel/EnclosingMethodAttributeTest.java | 5 +-
.../apache/bcel/generic/FieldAnnotationsTest.java | 5 +-
.../generic/GeneratingAnnotatedClassesTest.java | 5 +-
.../bcel/util/RepositoryNameMismatchTest.java | 58 ++++++++++++++++++++++
6 files changed, 79 insertions(+), 6 deletions(-)
diff --git
a/src/main/java/org/apache/bcel/util/AbstractClassPathRepository.java
b/src/main/java/org/apache/bcel/util/AbstractClassPathRepository.java
index 60f56590..429c156a 100644
--- a/src/main/java/org/apache/bcel/util/AbstractClassPathRepository.java
+++ b/src/main/java/org/apache/bcel/util/AbstractClassPathRepository.java
@@ -89,6 +89,12 @@ abstract class AbstractClassPathRepository implements
Repository {
if (inputStream != null) {
final ClassParser parser = new ClassParser(inputStream,
className);
final JavaClass clazz = parser.parse();
+ // The this_class name inside the parsed bytes is
attacker-controlled: caching it under its own name
+ // would let a class file found under one name poison the
repository entry for another (the JDK's
+ // defineClass() rejects the same mismatch). Refuse to cache
or return it.
+ if (!clazz.getClassName().equals(className)) {
+ throw new ClassNotFoundException("Class name mismatch:
requested " + className + " but the class file declares " +
clazz.getClassName());
+ }
storeClass(clazz);
return clazz;
}
diff --git a/src/main/java/org/apache/bcel/util/ClassLoaderRepository.java
b/src/main/java/org/apache/bcel/util/ClassLoaderRepository.java
index 52e207fd..c1675e1e 100644
--- a/src/main/java/org/apache/bcel/util/ClassLoaderRepository.java
+++ b/src/main/java/org/apache/bcel/util/ClassLoaderRepository.java
@@ -93,6 +93,12 @@ public class ClassLoaderRepository implements Repository {
}
final ClassParser parser = new ClassParser(is, className);
RC = parser.parse();
+ // The this_class name inside the parsed bytes is
attacker-controlled: caching it under its own name
+ // would let a class file found under one name poison the
repository entry for another (the JDK's
+ // defineClass() rejects the same mismatch). Refuse to cache or
return it.
+ if (!RC.getClassName().equals(className)) {
+ throw new ClassNotFoundException("Class name mismatch:
requested " + className + " but the class file declares " + RC.getClassName());
+ }
storeClass(RC);
return RC;
} catch (final IOException e) {
diff --git a/src/test/java/org/apache/bcel/EnclosingMethodAttributeTest.java
b/src/test/java/org/apache/bcel/EnclosingMethodAttributeTest.java
index 9fdfdf87..3e6a3f95 100644
--- a/src/test/java/org/apache/bcel/EnclosingMethodAttributeTest.java
+++ b/src/test/java/org/apache/bcel/EnclosingMethodAttributeTest.java
@@ -39,7 +39,8 @@ class EnclosingMethodAttributeTest extends AbstractTest {
*/
@Test
void testAttributeSerializtion() throws ClassNotFoundException,
IOException {
- final JavaClass clazz = getTestJavaClass(PACKAGE_BASE_NAME +
".data.AttributeTestClassEM02$1");
+ final String name = PACKAGE_BASE_NAME +
".data.AttributeTestClassEM02$1";
+ final JavaClass clazz = getTestJavaClass(name);
final ConstantPool pool = clazz.getConstantPool();
final Attribute[] encMethodAttrs = findAttribute("EnclosingMethod",
clazz);
assertEquals(1, encMethodAttrs.length, "Wrong number of
EnclosingMethod attributes");
@@ -48,7 +49,7 @@ class EnclosingMethodAttributeTest extends AbstractTest {
clazz.dump(tfile);
// Read in the new version and check it is OK
final SyntheticRepository repos2 = createRepos(".");
- final JavaClass clazz2 = repos2.loadClass("AttributeTestClassEM02$1");
+ final JavaClass clazz2 = repos2.loadClass(name);
assertNotNull(clazz2); // Use the variable to avoid a warning
final EnclosingMethod em = (EnclosingMethod) encMethodAttrs[0];
final String enclosingClassName =
em.getEnclosingClass().getBytes(pool);
diff --git a/src/test/java/org/apache/bcel/generic/FieldAnnotationsTest.java
b/src/test/java/org/apache/bcel/generic/FieldAnnotationsTest.java
index 5a05c552..37302716 100644
--- a/src/test/java/org/apache/bcel/generic/FieldAnnotationsTest.java
+++ b/src/test/java/org/apache/bcel/generic/FieldAnnotationsTest.java
@@ -75,14 +75,15 @@ class FieldAnnotationsTest extends AbstractTest {
*/
@Test
void testFieldAnnotationEntrysReadWrite() throws ClassNotFoundException,
IOException {
- final JavaClass clazz = getTestJavaClass(PACKAGE_BASE_NAME +
".data.AnnotatedFields");
+ final String name = PACKAGE_BASE_NAME + ".data.AnnotatedFields";
+ final JavaClass clazz = getTestJavaClass(name);
checkAnnotatedField(clazz, "i", "L" + PACKAGE_BASE_SIG +
"/data/SimpleAnnotation;", "id", "1");
checkAnnotatedField(clazz, "s", "L" + PACKAGE_BASE_SIG +
"/data/SimpleAnnotation;", "id", "2");
// Write it out
final File tfile = createTestdataFile("AnnotatedFields.class");
clazz.dump(tfile);
final SyntheticRepository repos2 = createRepos(".");
- repos2.loadClass("AnnotatedFields");
+ repos2.loadClass(name);
checkAnnotatedField(clazz, "i", "L" + PACKAGE_BASE_SIG +
"/data/SimpleAnnotation;", "id", "1");
checkAnnotatedField(clazz, "s", "L" + PACKAGE_BASE_SIG +
"/data/SimpleAnnotation;", "id", "2");
assertTrue(tfile.delete());
diff --git
a/src/test/java/org/apache/bcel/generic/GeneratingAnnotatedClassesTest.java
b/src/test/java/org/apache/bcel/generic/GeneratingAnnotatedClassesTest.java
index f3e007b3..ddcd7ca5 100644
--- a/src/test/java/org/apache/bcel/generic/GeneratingAnnotatedClassesTest.java
+++ b/src/test/java/org/apache/bcel/generic/GeneratingAnnotatedClassesTest.java
@@ -455,13 +455,14 @@ class GeneratingAnnotatedClassesTest extends AbstractTest
{
*/
@Test
void testModifyingClasses2() throws ClassNotFoundException {
- final JavaClass jc = getTestJavaClass(PACKAGE_BASE_NAME +
".data.SimpleAnnotatedClass");
+ final String name = PACKAGE_BASE_NAME + ".data.SimpleAnnotatedClass";
+ final JavaClass jc = getTestJavaClass(name);
final ClassGen cgen = new ClassGen(jc);
final ConstantPoolGen cp = cgen.getConstantPool();
cgen.addAnnotationEntry(createCombinedAnnotation(cp));
assertEquals(2, cgen.getAnnotationEntries().length, "Wrong number of
annotations");
dumpClass(cgen, "SimpleAnnotatedClass.class");
- final JavaClass jc2 = getClassFrom(".", "SimpleAnnotatedClass");
+ final JavaClass jc2 = getClassFrom(".", name);
jc2.getAnnotationEntries();
assertTrue(delete("SimpleAnnotatedClass.class"));
// System.err.println(jc2.toString());
diff --git a/src/test/java/org/apache/bcel/util/RepositoryNameMismatchTest.java
b/src/test/java/org/apache/bcel/util/RepositoryNameMismatchTest.java
new file mode 100644
index 00000000..13f36721
--- /dev/null
+++ b/src/test/java/org/apache/bcel/util/RepositoryNameMismatchTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bcel.util;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import org.apache.bcel.Const;
+import org.apache.bcel.generic.ClassGen;
+import org.junit.jupiter.api.Test;
+
+/**
+ * A class file served under one name whose this_class declares another name
must not be returned or cached:
+ * repositories key their caches on the parsed name, so accepting the mismatch
lets attacker bytes squat on
+ * an arbitrary class name JVM-wide (including the static
org.apache.bcel.Repository facade and the
+ * VerifierFactory verdict cache).
+ */
+class RepositoryNameMismatchTest {
+
+ private static byte[] classBytes(final String className) {
+ return new ClassGen(className, "java.lang.Object", className +
".java", Const.ACC_PUBLIC, null).getJavaClass().getBytes();
+ }
+
+ @Test
+ void testClassLoaderRepositoryRejectsMismatchedName() {
+ final byte[] bytes = classBytes("evil.Squatter");
+ final ClassLoader loader = new
ClassLoader(getClass().getClassLoader()) {
+ @Override
+ public InputStream getResourceAsStream(final String name) {
+ return "victim/Innocent.class".equals(name) ? new
ByteArrayInputStream(bytes) : super.getResourceAsStream(name);
+ }
+ };
+ final ClassLoaderRepository repository = new
ClassLoaderRepository(loader);
+ assertThrows(ClassNotFoundException.class, () ->
repository.loadClass("victim.Innocent"));
+ assertNull(repository.findClass("evil.Squatter"), "mismatched class
must not be cached under its declared name");
+ assertNull(repository.findClass("victim.Innocent"), "mismatched class
must not be cached under the requested name");
+ }
+}