Skip to content

Commit 0aa13da

Browse files
lukaszlenartclaude
andcommitted
WW-5622 docs: add spec and implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2eede24 commit 0aa13da

2 files changed

Lines changed: 182 additions & 0 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# WW-5622: Cache Hibernate Class Presence in ProxyUtil
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Eliminate repeated `NoClassDefFoundError` exceptions in `ProxyUtil` when Hibernate is absent from the classpath, fixing severe performance degradation.
6+
7+
**Architecture:** Add a one-time `Class.forName()` check at class-load time stored in a static `boolean HIBERNATE_AVAILABLE`. Guard all three Hibernate-touching methods with this flag to short-circuit immediately when Hibernate is absent.
8+
9+
**Tech Stack:** Java, JUnit 4
10+
11+
**JIRA:** [WW-5622](https://issues.apache.org/jira/browse/WW-5622)
12+
**Spec:** `docs/superpowers/specs/2026-04-02-proxyutil-hibernate-presence-cache-design.md`
13+
14+
---
15+
16+
### File Map
17+
18+
- **Modify:** `core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java` — add `HIBERNATE_AVAILABLE` field, `detectHibernate()` method, and guard checks in 3 methods
19+
20+
---
21+
22+
### Task 1: Add HIBERNATE_AVAILABLE detection and guard all Hibernate methods
23+
24+
**Files:**
25+
- Modify: `core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java:46-53` (add field after constants)
26+
- Modify: `core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java:155-161` (guard `isHibernateProxy`)
27+
- Modify: `core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java:168-176` (guard `isHibernateProxyMember`)
28+
- Modify: `core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java:305-311` (guard `getHibernateProxyTarget`)
29+
30+
- [ ] **Step 1: Add the static detection field and method**
31+
32+
After line 52 (`private static final int CACHE_INITIAL_CAPACITY = 256;`), add:
33+
34+
```java
35+
private static final boolean HIBERNATE_AVAILABLE = detectHibernate();
36+
37+
private static boolean detectHibernate() {
38+
try {
39+
Class.forName("org.hibernate.proxy.HibernateProxy");
40+
return true;
41+
} catch (ClassNotFoundException e) {
42+
return false;
43+
}
44+
}
45+
```
46+
47+
- [ ] **Step 2: Guard `isHibernateProxy`**
48+
49+
In `isHibernateProxy(Object object)` (line 155), add guard before the try block. The method becomes:
50+
51+
```java
52+
public static boolean isHibernateProxy(Object object) {
53+
if (!HIBERNATE_AVAILABLE) return false;
54+
try {
55+
return object != null && HibernateProxy.class.isAssignableFrom(object.getClass());
56+
} catch (NoClassDefFoundError ignored) {
57+
return false;
58+
}
59+
}
60+
```
61+
62+
- [ ] **Step 3: Guard `isHibernateProxyMember`**
63+
64+
In `isHibernateProxyMember(Member member)` (line 168), add guard before the try block. The method becomes:
65+
66+
```java
67+
public static boolean isHibernateProxyMember(Member member) {
68+
if (!HIBERNATE_AVAILABLE) return false;
69+
try {
70+
Class<?> clazz = ClassLoaderUtil.loadClass(HIBERNATE_HIBERNATEPROXY_CLASS_NAME, ProxyUtil.class);
71+
return hasMember(clazz, member);
72+
} catch (ClassNotFoundException ignored) {
73+
}
74+
75+
return false;
76+
}
77+
```
78+
79+
- [ ] **Step 4: Guard `getHibernateProxyTarget`**
80+
81+
In `getHibernateProxyTarget(Object object)` (line 305), add guard before the try block. The method becomes:
82+
83+
```java
84+
public static Object getHibernateProxyTarget(Object object) {
85+
if (!HIBERNATE_AVAILABLE) return object;
86+
try {
87+
return Hibernate.unproxy(object);
88+
} catch (NoClassDefFoundError ignored) {
89+
return object;
90+
}
91+
}
92+
```
93+
94+
- [ ] **Step 5: Run existing tests to verify no regression**
95+
96+
Run: `./mvnw -pl core clean test -Dtest=com.opensymphony.xwork2.util.** -DfailIfNoTests=false`
97+
Expected: All tests PASS. Since Hibernate IS on the test classpath, `HIBERNATE_AVAILABLE` will be `true` and all code paths execute as before.
98+
99+
Run: `./mvnw -pl plugins/spring clean test -Dtest=com.opensymphony.xwork2.spring.SpringProxyUtilTest`
100+
Expected: All 3 test methods PASS.
101+
102+
- [ ] **Step 6: Commit**
103+
104+
```bash
105+
git add core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java
106+
git commit -m "WW-5622 perf(core): cache Hibernate class presence to avoid repeated NoClassDefFoundError
107+
108+
Detect Hibernate availability once at class-load time via Class.forName()
109+
and short-circuit all Hibernate-related methods immediately when absent.
110+
This eliminates repeated NoClassDefFoundError exceptions that cause
111+
significant performance degradation in applications without Hibernate
112+
on the classpath."
113+
```
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# ProxyUtil: Cache Hibernate Class Presence to Avoid NoClassDefFoundError Performance Penalty
2+
3+
**Date:** 2026-04-02
4+
**JIRA:** [WW-5622](https://issues.apache.org/jira/browse/WW-5622)
5+
**Issue:** CVE-2026-0603 (hibernate-core 5.x), performance degradation from repeated NoClassDefFoundError
6+
**Scope:** `core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java`
7+
8+
## Problem
9+
10+
`ProxyUtil` has compile-time imports of `org.hibernate.proxy.HibernateProxy` and `org.hibernate.Hibernate`. Since `hibernate-core` is an `<optional>` dependency of `struts-core`, most applications don't have it on the classpath.
11+
12+
When Hibernate is absent, every call to `isHibernateProxy()` throws and catches a `NoClassDefFoundError`. Since `isProxy()` and `isProxyMember()` are invoked on every OGNL evaluation, this produces hundreds of thousands of caught errors per request cycle, severely degrading performance.
13+
14+
Note: `isProxy()` results are cached per class, but `isProxyMember()` at line 135 calls `isHibernateProxy()` directly (outside the cache check), so the error is thrown on every unique `(member, object)` pair.
15+
16+
## Solution
17+
18+
Add a static `boolean hibernateAvailable` field to `ProxyUtil`, resolved once at class-load time. All Hibernate-touching methods check this flag first and short-circuit immediately when Hibernate is absent.
19+
20+
### Implementation
21+
22+
Add to `ProxyUtil`:
23+
24+
```java
25+
private static final boolean HIBERNATE_AVAILABLE = detectHibernate();
26+
27+
private static boolean detectHibernate() {
28+
try {
29+
Class.forName("org.hibernate.proxy.HibernateProxy");
30+
return true;
31+
} catch (ClassNotFoundException e) {
32+
return false;
33+
}
34+
}
35+
```
36+
37+
Guard each Hibernate method with the flag:
38+
39+
1. **`isHibernateProxy(Object object)`** (line 155) — add `if (!HIBERNATE_AVAILABLE) return false;` before the try block.
40+
2. **`isHibernateProxyMember(Member member)`** (line 168) — add `if (!HIBERNATE_AVAILABLE) return false;` before the try block.
41+
3. **`getHibernateProxyTarget(Object object)`** (line 305) — add `if (!HIBERNATE_AVAILABLE) return object;` before the try block.
42+
43+
### What stays the same
44+
45+
- Compile-time imports of `HibernateProxy` and `Hibernate` remain (JVM won't resolve them unless the guarded code paths execute).
46+
- Existing `isProxyCache` and `isProxyMemberCache` caching remains.
47+
- The `catch (NoClassDefFoundError)` blocks remain as defensive safety nets (they just won't fire anymore).
48+
- Behavior when Hibernate IS present is completely unchanged.
49+
50+
### Difference from Struts 7 PR (#1649)
51+
52+
The Struts 7 PR catches `LinkageError` (broader). We intentionally keep catching only `NoClassDefFoundError` (narrower). If Hibernate jars are on the classpath but have missing transitive dependencies, that's a real deployment problem — the resulting `LinkageError` should propagate to the user rather than being silently swallowed.
53+
54+
## Testing
55+
56+
- Existing `ProxyUtil` tests pass unchanged (Hibernate is on the test classpath via the optional dependency).
57+
- The class-load-time check is inherently validated by any environment without Hibernate on the classpath.
58+
59+
## User-Facing Advice (for CVE-2026-0603 reporters)
60+
61+
Upgrading `hibernate-core` from 5.6.15 to any later 5.6.x or 5.7.x release is safe with Struts 6.8.0. Struts only uses:
62+
- `org.hibernate.proxy.HibernateProxy` (interface assignability check)
63+
- `Hibernate.unproxy()` (static utility method)
64+
65+
Both are stable across Hibernate 5.x. Do NOT upgrade to Hibernate 6.x — the package namespace changed from `org.hibernate` to `org.hibernate.orm`.
66+
67+
## Files Modified
68+
69+
- `core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java` — add static initializer and guard checks

0 commit comments

Comments
 (0)