[
https://issues.apache.org/jira/browse/JDO-861?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Tilmann Zäschke updated JDO-861:
--------------------------------
Description:
| # | Severity | File | Issue |
|---|----------|------|-------|
| 1 | Bug | `identity/IntIdentity.java:131` | `compareTo` integer subtraction
overflows |
| 2 | Bug / Security | `LegacyJava.java:112` | `checkPermission` invokes
instance method with `null` receiver |
| 3 | Security | `JDOHelper.java:1156` | `setExpandEntityReferences(true)`
weakens XML hardening |
| 4 | Code Quality | `JDOHelper.java:1593` | Deprecated `Class.newInstance()` |
| 5 | Thread Safety | `JDOException.java:238` | `inPrintStackTrace` flag locked
on wrong monitor |
| 6 | Minor | `identity/ObjectIdentity.java:157` | `hashCode()` omits
class-name contribution |
---
h1. Finding 1 — Integer overflow in `IntIdentity.compareTo`
**Severity:** Bug
**File:** `api/src/main/java/javax/jdo/identity/IntIdentity.java`, line 131
h2. Description
The `compareTo` method computes ordering using integer subtraction:
```java
return (result == 0) ? (key - o.key) : result;
```
`key - o.key` overflows when the values sit at opposite extremes. For example,
if
`key = Integer.MIN_VALUE` and `o.key = 1`, the subtraction wraps around to
`Integer.MAX_VALUE`, producing a positive result when the correct answer is
negative.
This silently corrupts the ordering of any sorted collection (`TreeSet`,
`TreeMap`, etc.)
that contains `IntIdentity` instances with large positive or negative keys.
All other narrow-type identity classes (`ByteIdentity`, `CharIdentity`,
`ShortIdentity`)
are safe because their types are widened to `int` before subtraction, and
`LongIdentity`
already avoids subtraction entirely with an explicit sign-comparison.
`IntIdentity` is the
only class in the hierarchy that is actually broken.
h2. Fix
Replace the subtraction with `Integer.compare`:
```java
// Before
return (result == 0) ? (key - o.key) : result;
// After
return (result == 0) ? Integer.compare(key, o.key) : result;
```
---
h1. Finding 2 — Wrong receiver in `LegacyJava.SecurityManager.checkPermission`
**Severity:** Bug / Security
**File:** `api/src/main/java/javax/jdo/LegacyJava.java`, line 112
h2. Description
The inner `SecurityManager` class stores the real Java `SecurityManager`
instance in the
field `sm` and looks up the `checkPermission` method reflectively. However,
when the
method is invoked, `null` is passed as the target instance:
```java
public void checkPermission(JDOPermission permission) {
try {
checkPermissionMethod.invoke(null, permission); // ← should be `sm`
} catch (IllegalAccessException | InvocationTargetException e) {
throw new JDOFatalInternalException(e.getMessage());
}
}
```
`java.lang.SecurityManager.checkPermission` is an *instance* method. Passing
`null`
as the receiver to `Method.invoke` causes a `NullPointerException` (wrapped in
`InvocationTargetException`), which is caught and re-thrown as a
`JDOFatalInternalException`. The security check therefore **never executes**;
it always
throws an exception instead.
This affects pre-Java-17 JVMs where the `SecurityManager` is still active. On
Java 17+
`IS_SECURITY_DEPRECATED` is `true` and this code path is skipped entirely.
h2. Fix
Pass the stored `sm` reference as the receiver:
```java
// Before
checkPermissionMethod.invoke(null, permission);
// After
checkPermissionMethod.invoke(sm, permission);
```
---
h1. Finding 3 — `setExpandEntityReferences(true)` weakens XML hardening
**Severity:** Security
**File:** `api/src/main/java/javax/jdo/JDOHelper.java`, line 1156
h2. Description
`getDefaultDocumentBuilderFactory` correctly disables DOCTYPE declarations to
prevent
XXE injection:
```java
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",
true);
```
However, it immediately after enables entity reference expansion:
```java
factory.setExpandEntityReferences(true);
```
While the `disallow-doctype-decl` feature prevents external entity injection via
DOCTYPE, leaving `setExpandEntityReferences` set to `true` is contrary to
[OWASP's secure XML processing
recommendations](https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html)
and partially undermines the hardening intent. Additionally,
`disallow-doctype-decl` is an
Apache Xerces-specific feature; if an alternative XML parser is used it will
throw a
`ParserConfigurationException`. While this is handled (a
`JDOFatalUserException` is thrown),
defence-in-depth is better served by setting entity expansion to `false`
regardless.
h2. Fix
```java
// Before
factory.setExpandEntityReferences(true);
// After
factory.setExpandEntityReferences(false);
```
---
h1. Finding 4 — Deprecated `Class.newInstance()` in `getEnhancer`
**Severity:** Code Quality
**File:** `api/src/main/java/javax/jdo/JDOHelper.java`, line 1593
h2. Description
The enhancer is instantiated using the API deprecated since Java 9:
```java
return (JDOEnhancer) enhancerClass.newInstance();
```
`Class.newInstance()` has two well-known problems:
1. It propagates *checked* exceptions thrown by the no-arg constructor as
unchecked
exceptions, bypassing the compiler's exception-checking mechanism and making
error
handling unpredictable.
2. It bypasses the explicit constructor-accessibility checks that
`getDeclaredConstructor().newInstance()` enforces.
The existing `catch (Exception ex)` block already handles
`ReflectiveOperationException`,
so the replacement requires no changes to error handling.
h2. Fix
```java
// Before
return (JDOEnhancer) enhancerClass.newInstance();
// After
return (JDOEnhancer) enhancerClass.getDeclaredConstructor().newInstance();
```
---
h1. Finding 5 — Thread-safety issue with `inPrintStackTrace` in `JDOException`
**Severity:** Thread Safety
**File:** `api/src/main/java/javax/jdo/JDOException.java`, lines 236–278
h2. Description
`inPrintStackTrace` is an instance field that controls behaviour in both
`toString()` and
`getCause()`. It is set inside a block synchronised on the output *stream* `s`:
```java
synchronized (s) {
inPrintStackTrace = true;
super.printStackTrace(s);
// ...
inPrintStackTrace = false;
}
```
If two threads call `printStackTrace` on the **same exception instance** with
**different streams**, both acquire different monitors and both mutate
`inPrintStackTrace`
concurrently without any coordination. The result is a race condition:
`getCause()` may
return `null` when it should not (or vice versa), and the nested-throwable
block in
`toString()` may be incorrectly included or suppressed during concurrent
printing.
The `@SuppressWarnings("java:S2445")` annotation acknowledges that Sonar flags
this
pattern, but the suppression hides a genuine concurrency defect.
h2. Fix
Lock on the exception instance itself, keeping stream-level synchronisation
inside:
```java
@Override
public void printStackTrace(java.io.PrintStream s) {
int len = nested == null ? 0 : nested.length;
synchronized (this) {
inPrintStackTrace = true;
try {
super.printStackTrace(s);
if (len > 0) {
s.println(MSG.msg("MSG_NestedThrowablesStackTrace"));
for (int i = 0; i < len; ++i) {
Throwable exception = nested[i];
if (exception != null) {
exception.printStackTrace(s);
}
}
}
} finally {
inPrintStackTrace = false;
}
}
}
```
The same change applies to the `PrintWriter` overload.
---
h1. Finding 6 — `ObjectIdentity.hashCode()` omits class-name contribution
**Severity:** Minor
**File:** `api/src/main/java/javax/jdo/identity/ObjectIdentity.java`, line 157
h2. Description
Every other `SingleFieldIdentity` subclass computes its hash code as
`hashClassName() ^ key`, stored in the inherited `hashCode` field.
`ObjectIdentity`
overrides `hashCode()` to return only the key object's hash:
```java
@Override
public int hashCode() {
return keyAsObject.hashCode(); // class name not included
}
```
The `hashCode` field is still *set* correctly in the constructor as
`hashClassName() ^ keyAsObject.hashCode()`, but is never returned. The practical
consequence is that two `ObjectIdentity` instances for *different* entity
classes but with
the same key value always produce the same hash code, causing guaranteed bucket
collisions in any `HashMap` or `HashSet` that mixes identity types. This does
not violate
the `equals`/`hashCode` contract (unequal objects may share a hash), but it
degrades
performance in collections containing identities for multiple entity types.
h2. Fix
Return the pre-computed field that already incorporates the class name:
```java
// Before
@Override
public int hashCode() {
return keyAsObject.hashCode();
}
// After
@Override
public int hashCode() {
return hashCode; // hashClassName() ^ keyAsObject.hashCode(), set in
constructor
}
```
> List of bug reports
> -------------------
>
> Key: JDO-861
> URL: https://issues.apache.org/jira/browse/JDO-861
> Project: JDO
> Issue Type: Bug
> Components: api
> Affects Versions: JDO 3.2.1
> Reporter: Tilmann Zäschke
> Priority: Major
>
> | # | Severity | File | Issue |
> |---|----------|------|-------|
> | 1 | Bug | `identity/IntIdentity.java:131` | `compareTo` integer subtraction
> overflows |
> | 2 | Bug / Security | `LegacyJava.java:112` | `checkPermission` invokes
> instance method with `null` receiver |
> | 3 | Security | `JDOHelper.java:1156` | `setExpandEntityReferences(true)`
> weakens XML hardening |
> | 4 | Code Quality | `JDOHelper.java:1593` | Deprecated `Class.newInstance()`
> |
> | 5 | Thread Safety | `JDOException.java:238` | `inPrintStackTrace` flag
> locked on wrong monitor |
> | 6 | Minor | `identity/ObjectIdentity.java:157` | `hashCode()` omits
> class-name contribution |
> ---
> h1. Finding 1 — Integer overflow in `IntIdentity.compareTo`
> **Severity:** Bug
> **File:** `api/src/main/java/javax/jdo/identity/IntIdentity.java`, line 131
> h2. Description
> The `compareTo` method computes ordering using integer subtraction:
> ```java
> return (result == 0) ? (key - o.key) : result;
> ```
> `key - o.key` overflows when the values sit at opposite extremes. For
> example, if
> `key = Integer.MIN_VALUE` and `o.key = 1`, the subtraction wraps around to
> `Integer.MAX_VALUE`, producing a positive result when the correct answer is
> negative.
> This silently corrupts the ordering of any sorted collection (`TreeSet`,
> `TreeMap`, etc.)
> that contains `IntIdentity` instances with large positive or negative keys.
> All other narrow-type identity classes (`ByteIdentity`, `CharIdentity`,
> `ShortIdentity`)
> are safe because their types are widened to `int` before subtraction, and
> `LongIdentity`
> already avoids subtraction entirely with an explicit sign-comparison.
> `IntIdentity` is the
> only class in the hierarchy that is actually broken.
> h2. Fix
> Replace the subtraction with `Integer.compare`:
> ```java
> // Before
> return (result == 0) ? (key - o.key) : result;
> // After
> return (result == 0) ? Integer.compare(key, o.key) : result;
> ```
> ---
> h1. Finding 2 — Wrong receiver in `LegacyJava.SecurityManager.checkPermission`
> **Severity:** Bug / Security
> **File:** `api/src/main/java/javax/jdo/LegacyJava.java`, line 112
> h2. Description
> The inner `SecurityManager` class stores the real Java `SecurityManager`
> instance in the
> field `sm` and looks up the `checkPermission` method reflectively. However,
> when the
> method is invoked, `null` is passed as the target instance:
> ```java
> public void checkPermission(JDOPermission permission) {
> try {
> checkPermissionMethod.invoke(null, permission); // ← should be `sm`
> } catch (IllegalAccessException | InvocationTargetException e) {
> throw new JDOFatalInternalException(e.getMessage());
> }
> }
> ```
> `java.lang.SecurityManager.checkPermission` is an *instance* method. Passing
> `null`
> as the receiver to `Method.invoke` causes a `NullPointerException` (wrapped in
> `InvocationTargetException`), which is caught and re-thrown as a
> `JDOFatalInternalException`. The security check therefore **never executes**;
> it always
> throws an exception instead.
> This affects pre-Java-17 JVMs where the `SecurityManager` is still active. On
> Java 17+
> `IS_SECURITY_DEPRECATED` is `true` and this code path is skipped entirely.
> h2. Fix
> Pass the stored `sm` reference as the receiver:
> ```java
> // Before
> checkPermissionMethod.invoke(null, permission);
> // After
> checkPermissionMethod.invoke(sm, permission);
> ```
> ---
> h1. Finding 3 — `setExpandEntityReferences(true)` weakens XML hardening
> **Severity:** Security
> **File:** `api/src/main/java/javax/jdo/JDOHelper.java`, line 1156
> h2. Description
> `getDefaultDocumentBuilderFactory` correctly disables DOCTYPE declarations to
> prevent
> XXE injection:
> ```java
> factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",
> true);
> ```
> However, it immediately after enables entity reference expansion:
> ```java
> factory.setExpandEntityReferences(true);
> ```
> While the `disallow-doctype-decl` feature prevents external entity injection
> via
> DOCTYPE, leaving `setExpandEntityReferences` set to `true` is contrary to
> [OWASP's secure XML processing
> recommendations](https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html)
> and partially undermines the hardening intent. Additionally,
> `disallow-doctype-decl` is an
> Apache Xerces-specific feature; if an alternative XML parser is used it will
> throw a
> `ParserConfigurationException`. While this is handled (a
> `JDOFatalUserException` is thrown),
> defence-in-depth is better served by setting entity expansion to `false`
> regardless.
> h2. Fix
> ```java
> // Before
> factory.setExpandEntityReferences(true);
> // After
> factory.setExpandEntityReferences(false);
> ```
> ---
> h1. Finding 4 — Deprecated `Class.newInstance()` in `getEnhancer`
> **Severity:** Code Quality
> **File:** `api/src/main/java/javax/jdo/JDOHelper.java`, line 1593
> h2. Description
> The enhancer is instantiated using the API deprecated since Java 9:
> ```java
> return (JDOEnhancer) enhancerClass.newInstance();
> ```
> `Class.newInstance()` has two well-known problems:
> 1. It propagates *checked* exceptions thrown by the no-arg constructor as
> unchecked
> exceptions, bypassing the compiler's exception-checking mechanism and
> making error
> handling unpredictable.
> 2. It bypasses the explicit constructor-accessibility checks that
> `getDeclaredConstructor().newInstance()` enforces.
> The existing `catch (Exception ex)` block already handles
> `ReflectiveOperationException`,
> so the replacement requires no changes to error handling.
> h2. Fix
> ```java
> // Before
> return (JDOEnhancer) enhancerClass.newInstance();
> // After
> return (JDOEnhancer) enhancerClass.getDeclaredConstructor().newInstance();
> ```
> ---
> h1. Finding 5 — Thread-safety issue with `inPrintStackTrace` in `JDOException`
> **Severity:** Thread Safety
> **File:** `api/src/main/java/javax/jdo/JDOException.java`, lines 236–278
> h2. Description
> `inPrintStackTrace` is an instance field that controls behaviour in both
> `toString()` and
> `getCause()`. It is set inside a block synchronised on the output *stream*
> `s`:
> ```java
> synchronized (s) {
> inPrintStackTrace = true;
> super.printStackTrace(s);
> // ...
> inPrintStackTrace = false;
> }
> ```
> If two threads call `printStackTrace` on the **same exception instance** with
> **different streams**, both acquire different monitors and both mutate
> `inPrintStackTrace`
> concurrently without any coordination. The result is a race condition:
> `getCause()` may
> return `null` when it should not (or vice versa), and the nested-throwable
> block in
> `toString()` may be incorrectly included or suppressed during concurrent
> printing.
> The `@SuppressWarnings("java:S2445")` annotation acknowledges that Sonar
> flags this
> pattern, but the suppression hides a genuine concurrency defect.
> h2. Fix
> Lock on the exception instance itself, keeping stream-level synchronisation
> inside:
> ```java
> @Override
> public void printStackTrace(java.io.PrintStream s) {
> int len = nested == null ? 0 : nested.length;
> synchronized (this) {
> inPrintStackTrace = true;
> try {
> super.printStackTrace(s);
> if (len > 0) {
> s.println(MSG.msg("MSG_NestedThrowablesStackTrace"));
> for (int i = 0; i < len; ++i) {
> Throwable exception = nested[i];
> if (exception != null) {
> exception.printStackTrace(s);
> }
> }
> }
> } finally {
> inPrintStackTrace = false;
> }
> }
> }
> ```
> The same change applies to the `PrintWriter` overload.
> ---
> h1. Finding 6 — `ObjectIdentity.hashCode()` omits class-name contribution
> **Severity:** Minor
> **File:** `api/src/main/java/javax/jdo/identity/ObjectIdentity.java`, line 157
> h2. Description
> Every other `SingleFieldIdentity` subclass computes its hash code as
> `hashClassName() ^ key`, stored in the inherited `hashCode` field.
> `ObjectIdentity`
> overrides `hashCode()` to return only the key object's hash:
> ```java
> @Override
> public int hashCode() {
> return keyAsObject.hashCode(); // class name not included
> }
> ```
> The `hashCode` field is still *set* correctly in the constructor as
> `hashClassName() ^ keyAsObject.hashCode()`, but is never returned. The
> practical
> consequence is that two `ObjectIdentity` instances for *different* entity
> classes but with
> the same key value always produce the same hash code, causing guaranteed
> bucket
> collisions in any `HashMap` or `HashSet` that mixes identity types. This does
> not violate
> the `equals`/`hashCode` contract (unequal objects may share a hash), but it
> degrades
> performance in collections containing identities for multiple entity types.
> h2. Fix
> Return the pre-computed field that already incorporates the class name:
> ```java
> // Before
> @Override
> public int hashCode() {
> return keyAsObject.hashCode();
> }
> // After
> @Override
> public int hashCode() {
> return hashCode; // hashClassName() ^ keyAsObject.hashCode(), set in
> constructor
> }
> ```
--
This message was sent by Atlassian Jira
(v8.20.10#820010)