Skip to content

WW-5676 Keep arrays resolving to the empty package in OGNL security checks - #1858

Merged
lukaszlenart merged 1 commit into
mainfrom
WW-5676-array-package-resolution-decision
Aug 23, 2026
Merged

WW-5676 Keep arrays resolving to the empty package in OGNL security checks#1858
lukaszlenart merged 1 commit into
mainfrom
WW-5676-array-package-resolution-decision

Conversation

@lukaszlenart

Copy link
Copy Markdown
Member

Fixes WW-5676

WW-5676 asked whether SecurityMemberAccess.toPackageName should resolve arrays to the element type's package, and primitives/void to java.lang, instead of the empty package. The answer is no, and the premise the ticket was filed on is wrong. This PR changes no behaviour — it pins the reasoning so the question is not reopened from the same false premise.

The premise that failed

The ticket argued there was a defensive gap on the exclusion path:

A java.io.File[] currently has package "" and escapes struts.excludedPackageNames entirely, even though java.io is excluded by default.

and identified clone() as the way in:

for an array target, member.getDeclaringClass() is usually java.lang.Object, which is already in struts.excludedClasses; clone() is the notable exception, as its declaring class is the array type itself.

Array clone() is a JVM-internal method. The JLS gives array types a public clone(), but it is not in the reflection view. Verified on Temurin 17.0.14, 21.0.7 and 25.0.1, for String[], java.io.File[], int[] and Object[][]:

probe result
getMethods() exactly the 7 public java.lang.Object methods
getDeclaredMethods() []
getMethod("clone") NoSuchMethodException
getFields() / getField("length") [] / NoSuchFieldException

So every member reflectively reachable on an array class declares in java.lang.Object.

Why the package check is unreachable

java.lang.Object cannot be configured out of the exclusion list. It is the built-in default of SecurityMemberAccess.excludedClasses, and useExcludedClasses goes through ConfigParseUtil.toNewClassesSet, which accumulates onto the existing set rather than replacing it.

checkExclusionList tests isClassExcluded(member.getDeclaringClass()) before isPackageExcluded, so an array target is denied at the first check every time. An end-to-end check against a real SecurityMemberAccess with the production struts-excluded-classes.xml values and target = new java.io.File[]{...} denies all 9 reachable members, with the allowlist both enabled and disabled.

Why changing it would be a net loss

  • Exclusion path: tightens nothing, because it is unreachable.
  • Allowlist path: loosens genuinely. struts.allowlist.packageNames=com.app would begin implicitly allowlisting com.app.Thing[], which today requires an explicit struts.allowlist.classes entry. The allowlist is the primary OGNL defence in 7.x and is on by default.
  • Consistency: keeping "" also keeps toPackageName in agreement with checkDefaultPackageAccess's raw getPackage() == null reading, which is what WW-5677 wants to unify.

Primitives and void are moot either way: target.getClass() is never primitive and member.getDeclaringClass() is never primitive, so they cannot appear at either call site.

What this PR contains

  • SecurityMemberAccessArrayTargetTest (new, 4 tests) pins both facts the decision rests on, so either one breaking fails loudly rather than silently invalidating the reasoning:
    • everyReflectiveMemberOfAnArrayClassDeclaresInObject
    • arrayCloneIsNotReflectivelyReachable
    • objectStaysExcludedWhateverIsConfigured
    • noMemberOfAnArrayTargetIsAccessible — the payoff, over allowlist on and off
  • SecurityMemberAccess.toPackageName: the comment no longer states the false premise, and points at the test.

The two behavioural tests were mutation-checked: setting excludedClasses to emptySet() fails both, noMemberOfAnArrayTargetIsAccessible specifically at allowlistEnabled=false.

No production behaviour changes, no configuration changes, and no migration-guide note is needed. The existing arraysAndPrimitivesResolveToTheEmptyPackage and toPackageNameMatchesLegacyAcrossClassShapes in SecurityMemberAccessPackageMatchingTest continue to pin the behaviour itself and are untouched.

Testing

mvn test -DskipAssembly -pl core — 3185 tests, 0 failures, 0 errors.

🤖 Generated with Claude Code

WW-5676 asked whether toPackageName should resolve arrays to the element
type's package instead of the empty package. It should not, and the
premise the ticket was filed on turns out to be wrong.

The ticket assumed clone() is reachable on an array target with the array
type as its declaring class, leaving java.io.File[] free to slip past the
java.io entry in struts.excludedPackageNames. Array clone() is a
JVM-internal method and is absent from the reflection view: on Temurin 17,
21 and 25, getMethods() on an array class returns only java.lang.Object's
methods, getDeclaredMethods() is empty and getMethod("clone") throws.

java.lang.Object is in turn permanently excluded - it is the built-in
default of excludedClasses, and useExcludedClasses accumulates onto that
default rather than replacing it, so no configuration can drop it. Since
checkExclusionList tests the declaring class before the package, an array
target is always denied at the first check and the package comparison is
never reached.

Resolving arrays to the element package would therefore tighten nothing
while genuinely loosening the allowlist, so the behaviour stays as is.
This commit records the reasoning where it can rot loudly instead of
quietly: SecurityMemberAccessArrayTargetTest pins both facts the decision
rests on, and the comment on toPackageName no longer states the false
premise. No behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@lukaszlenart
lukaszlenart merged commit 015c9c8 into main Aug 23, 2026
13 checks passed
@lukaszlenart
lukaszlenart deleted the WW-5676-array-package-resolution-decision branch August 23, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant