View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.bcel.classfile;
21  
22  import static org.junit.jupiter.api.Assertions.assertNotNull;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  import static org.junit.jupiter.api.Assertions.fail;
25  
26  import java.io.ByteArrayInputStream;
27  import java.util.concurrent.Callable;
28  import java.util.concurrent.ExecutionException;
29  import java.util.concurrent.ExecutorService;
30  import java.util.concurrent.Executors;
31  import java.util.concurrent.Future;
32  import java.util.concurrent.TimeUnit;
33  import java.util.concurrent.TimeoutException;
34  
35  import org.apache.bcel.Repository;
36  import org.apache.bcel.util.SyntheticRepository;
37  import org.junit.jupiter.api.AfterEach;
38  import org.junit.jupiter.api.BeforeEach;
39  import org.junit.jupiter.api.Test;
40  
41  /**
42   * Tests {@link JavaClass}.
43   * <p>
44   * Tests for cyclic hierarchy vulnerabilities in {@link JavaClass}. These tests demonstrate CWE-674 (Uncontrolled Recursion) vulnerabilities:
45   * <ul>
46   * <li>getAllInterfaces(): infinite queue growth with cyclic interfaces</li>
47   * <li>getSuperClasses(): infinite loop with cyclic superclasses Without the fix, these tests will fail. With the fix, they pass.</li>
48   * </ul>
49   */
50  class JavaClassCyclicTest {
51  
52      private JavaClass cyclicClassA;
53  
54      private JavaClass cyclicClassB;
55  
56      private JavaClass cyclicInterfaceA;
57  
58      private JavaClass cyclicInterfaceB;
59  
60      private JavaClass cyclicTestClass;
61  
62      private SyntheticRepository repo;
63  
64      @BeforeEach
65      void setUp() throws Exception {
66          repo = SyntheticRepository.getInstance();
67          Repository.setRepository(repo);
68          // Create cyclic interfaces: A extends B, B extends A
69          final byte[] interfaceABytes = JavaClassTest.createInterface("CyclicInterfaceA", "CyclicInterfaceB");
70          final byte[] interfaceBBytes = JavaClassTest.createInterface("CyclicInterfaceB", "CyclicInterfaceA");
71          final byte[] testClassBytes = JavaClassTest.createClass("CyclicTestClass", "java.lang.Object", "CyclicInterfaceA");
72          cyclicInterfaceA = new ClassParser(new ByteArrayInputStream(interfaceABytes), "CyclicInterfaceA.class").parse();
73          cyclicInterfaceB = new ClassParser(new ByteArrayInputStream(interfaceBBytes), "CyclicInterfaceB.class").parse();
74          cyclicTestClass = new ClassParser(new ByteArrayInputStream(testClassBytes), "CyclicTestClass.class").parse();
75          repo.storeClass(cyclicInterfaceA);
76          repo.storeClass(cyclicInterfaceB);
77          repo.storeClass(cyclicTestClass);
78          // Create cyclic classes: A extends B, B extends A
79          final byte[] classABytes = JavaClassTest.createClass("CyclicClassA", "CyclicClassB");
80          final byte[] classBBytes = JavaClassTest.createClass("CyclicClassB", "CyclicClassA");
81          cyclicClassA = new ClassParser(new ByteArrayInputStream(classABytes), "CyclicClassA.class").parse();
82          cyclicClassB = new ClassParser(new ByteArrayInputStream(classBBytes), "CyclicClassB.class").parse();
83          repo.storeClass(cyclicClassA);
84          repo.storeClass(cyclicClassB);
85      }
86  
87      @AfterEach
88      void tearDown() {
89          if (cyclicInterfaceA != null) {
90              repo.removeClass(cyclicInterfaceA);
91          }
92          if (cyclicInterfaceB != null) {
93              repo.removeClass(cyclicInterfaceB);
94          }
95          if (cyclicTestClass != null) {
96              repo.removeClass(cyclicTestClass);
97          }
98          if (cyclicClassA != null) {
99              repo.removeClass(cyclicClassA);
100         }
101         if (cyclicClassB != null) {
102             repo.removeClass(cyclicClassB);
103         }
104     }
105 
106     void test(final Callable<JavaClass[]> callable) throws Exception {
107         final ExecutorService executor = Executors.newSingleThreadExecutor();
108         try {
109             final Future<JavaClass[]> future = executor.submit(callable);
110             // Without fix: will timeout (infinite loop)
111             // With fix: throws ClassCircularityError immediately
112             future.get(3, TimeUnit.SECONDS);
113             fail("Should have thrown ClassCircularityError for cyclic hierarchy");
114         } catch (final TimeoutException e) {
115             fail("Timeout: infinite loop vulnerability detected");
116         } catch (final ExecutionException e) {
117             if (e.getCause() instanceof ClassFormatException) {
118                 // Expected with fix - test passes
119                 return;
120             }
121             throw e;
122         } finally {
123             executor.shutdownNow();
124         }
125     }
126 
127     /**
128      * Tests that getAllInterfaces() handles cyclic interface hierarchies gracefully. BUG: Without fix, getAllInterfaces() has no visited-node check before
129      * enqueueing, causing infinite queue growth and eventual heap exhaustion (OutOfMemoryError). FIXED: With the fix, already-visited nodes are skipped,
130      * preventing infinite growth.
131      */
132     @Test
133     void testGetAllInterfacesCyclic() throws Exception {
134         // TODO Use the test method once ClassCircularityError is implemented for this case
135         // test(cyclicTestClass::getAllInterfaces);
136         // TOOO Remove once the above is used
137         final ExecutorService executor = Executors.newSingleThreadExecutor();
138         try {
139             final Future<JavaClass[]> future = executor.submit(() -> cyclicTestClass.getAllInterfaces());
140             // Without fix: will timeout (infinite queue growth) or throw OOM
141             // With fix: completes quickly and returns the interfaces
142             final JavaClass[] interfaces = future.get(3, TimeUnit.SECONDS);
143             assertNotNull(interfaces, "getAllInterfaces() should return non-null array");
144             assertTrue(interfaces.length >= 2, "Should find at least CyclicInterfaceA and CyclicInterfaceB");
145         } catch (final TimeoutException e) {
146             fail("getAllInterfaces() timed out - infinite queue growth vulnerability detected");
147         } catch (final ExecutionException e) {
148             if (e.getCause() instanceof OutOfMemoryError) {
149                 fail("getAllInterfaces() caused OutOfMemoryError - infinite queue growth vulnerability detected");
150             }
151             throw e;
152         } finally {
153             executor.shutdownNow();
154         }
155     }
156 
157     /**
158      * Tests that getSuperClasses() detects cyclic superclass hierarchies. BUG: Without fix, getSuperClasses() has no cycle detection, causing an infinite loop
159      * when traversing cyclic superclass chains. FIXED: With the fix, ClassCircularityError is thrown when a cycle is detected.
160      */
161     @Test
162     void testGetSuperClassesCyclic() throws Exception {
163         test(cyclicClassA::getSuperClasses);
164     }
165 }