Skip to content

WW-5697 Restrict the indexed-access fast path in XWorkMethodAccessor to real indexed properties - #1871

Open
lukaszlenart wants to merge 4 commits into
mainfrom
feature/WW-5697-indexed-access-fast-path
Open

WW-5697 Restrict the indexed-access fast path in XWorkMethodAccessor to real indexed properties#1871
lukaszlenart wants to merge 4 commits into
mainfrom
feature/WW-5697-indexed-access-fast-path

Conversation

@lukaszlenart

@lukaszlenart lukaszlenart commented Aug 27, 2026

Copy link
Copy Markdown
Member

Fixes WW-5697

Problem

XWorkMethodAccessor.callMethod(...) skipped the denyMethodExecution check for any method whose name began with get and took one argument, or set and took two:

//HACK - we pass indexed method access i.e. setXXX(A,B) pattern
if ((objects.length == 2 && string.startsWith("set")) || (objects.length == 1 && string.startsWith("get"))) {

That is a name prefix plus an argument count, not a property check. An ordinary method such as getSomething(String) is not a JavaBeans property, but it matches, so it was executed during parameter binding with the argument taken from the parameter name — a parameter name of getSomething('value').property calls getSomething("value").

The flag the branch consults, DENY_INDEXED_ACCESS_EXECUTION, is never written anywhere in main source, so exec is always null and the fast path is unconditional.

Change

The fast path now applies only where the call really is the indexed accessor of a property on the target type. The property name alone is not enough to decide that, which review of the first cut showed the hard way:

  • a class declaring the indexed pair getItem(int) / setItem(int, String) may also declare an unrelated getItem(String) overload, and a one-argument call dispatches to that overload — the argument types choose the method, and the caller chooses the arguments;
  • the check has to know its direction, or a read-only getItem(int) legitimises an unrelated two-argument setItem(String, String).

So the descriptor's own indexed accessor must be the method that will actually run: same name, same direction (getIndexedReadMethod / getIndexedWriteMethod, for both IndexedPropertyDescriptor and OGNL's ObjectIndexedPropertyDescriptor), and no same-arity overload for the dispatcher to prefer instead. Everything else falls through to the denyMethodExecution check.

The deny check is hoisted ahead of the indexed-property block, which it now guards. The two are equivalent — with execution permitted, both paths ended in the same callMethodWithDebugInfo call — but this way the introspection is skipped entirely on the common path, and the block reads as the exception it is.

DENY_INDEXED_ACCESS_EXECUTION is public API, so it is deprecated here rather than deleted; removal in 8.0.0 is tracked as WW-5699. It is still honoured, so the framework's own read of it carries @SuppressWarnings("removal") rather than warning on every core build.

Scope of the behaviour change

Confined to parameter binding. The fast path only matters when the deny flag is set, and that flag is set only by ParametersInterceptor, AliasInterceptor and StaticParametersInterceptor — with it unset, such a call already fell through to the check below and executed. JSP and tag rendering are unaffected.

Two narrowings worth calling out for the migration guide, both silent (debug-level) when they bite, and both only while method execution is denied:

  • An unpaired object-indexed getter is no longer exempt. String getKeyed(String) with no matching setKeyed(String, String) reports INDEXED_PROPERTY_NONE, because OGNL's findObjectIndexedPropertyDescriptors requires an exact pair with matching key and value types. The same applies when the types do not line up, e.g. String getKeyed(String) against void setKeyed(String, Object). OGNL itself refuses bean.keyed['k'] for such beans, so this is consistent, but it is a change.
  • Bare get(...) / set(...) are no longer exempt. "get".startsWith("get") matched the old fast path, so myMap.get('k').field bound under denyMethodExecution. There is no property name left once the prefix is stripped, so it cannot be an indexed accessor.

Tests

XWorkMethodAccessorTest is new and covers eight behaviours, including:

  • an argument-taking getter that is not an indexed property is not executed while method execution is denied
  • an overload of a genuine indexed accessor is not executed while denied
  • an unrelated two-argument setter named after a read-only indexed property is not executed while denied
  • object-indexed and int-indexed accessors still execute while denied
  • a bare get(String) is not executed while denied
  • with the deny flag unset, an argument-taking getter still executes, as before

Note that a getItem(int) / setItem(int, String) pair reports as INDEXED_PROPERTY_OBJECT, not _INT: findObjectIndexedPropertyDescriptors overwrites the java.beans descriptor whenever it finds a matching pair. A read-only int-indexed getter is the only shape that reaches the _INT branch, so there is a test for it specifically.

Mutation-checked both ways: restoring the old name-only predicate fails exactly the two overload/direction tests, and forcing the predicate to always reject fails exactly the three "still works" tests. None of them pass vacuously.

Full core suite green: 3205 tests, 0 failures, 0 errors.

Related

WW-5698 covers a separate issue found alongside this one — the ModelDriven exemption in StrutsParameterAuthorizer also exempting the action's own members. Different cause, different fix, not addressed here. An end-to-end ModelDriven binding test was deliberately left out of this PR because it would couple these tests to the exemption WW-5698 is expected to change.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Fixes WW-5697 by preventing XWorkMethodAccessor.callMethod(...) from bypassing denyMethodExecution for arbitrary get*/set* methods, restricting the “indexed-access” fast path to real indexed JavaBeans properties.

Changes:

  • Restricts the indexed-access fast path to methods backed by an indexed property on the target type (via OgnlRuntime.getIndexedPropertyType(...)).
  • Deprecates DENY_INDEXED_ACCESS_EXECUTION (public API) since it was never set by the framework and is no longer meaningful.
  • Adds a focused test suite covering blocked vs. allowed method execution under deny-mode, including both int- and object-indexed accessors.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
core/src/test/java/org/apache/struts2/ognl/accessor/XWorkMethodAccessorTest.java Adds regression tests for the indexed-access restriction and deny-mode behavior.
core/src/main/java/org/apache/struts2/util/reflection/ReflectionContextState.java Deprecates the now-obsolete indexed-access deny flag constant with rationale.
core/src/main/java/org/apache/struts2/ognl/accessor/XWorkMethodAccessor.java Implements indexed-property detection and applies it to gate the fast path.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread core/src/main/java/org/apache/struts2/util/reflection/ReflectionContextState.java Outdated
Comment thread core/src/main/java/org/apache/struts2/ognl/accessor/XWorkMethodAccessor.java Outdated
lukaszlenart and others added 4 commits August 27, 2026 18:40
…xed properties

XWorkMethodAccessor.callMethod skipped the denyMethodExecution check for any
method whose name began with "get" and took one argument, or "set" and took two.
That test is a name prefix plus an argument count, not a property check, so an
ordinary method such as getSomething(String) qualified and was executed during
parameter binding with the argument supplied in the parameter name.

The fast path now applies only where the target type genuinely declares an
indexed property accessor, determined with OgnlRuntime.getIndexedPropertyType.
Anything else falls through to the existing denyMethodExecution check.

Both int-indexed and object-indexed accessors continue to work. The new tests
cover those two, the argument-taking method that must now be blocked while
method execution is denied, and the unset-flag path where methods still execute
as before, so the change is confined to parameter binding.

DENY_INDEXED_ACCESS_EXECUTION is left in place for now; it is public API and is
never set anywhere, so its removal is handled separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing in the framework has ever written this key, so the check it guarded in
XWorkMethodAccessor never fired. Now that indexed property access is identified
from the target type rather than from a method name prefix, the flag has nothing
left to guard.

It is public API, so it is deprecated here rather than deleted, and removal is
tracked for 8.0.0 in WW-5699.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merge the nested indexed-property check into the enclosing condition (S1066)
and give the deprecation its since/forRemoval arguments (S6355).

Also cover the branch that rejects a method with nothing left after the "get"
prefix, using a map style get(String) accessor. That is worth asserting in its
own right: such a method is not an indexed property accessor, so it must not be
executed while method execution is denied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…operty name

Addresses review of #1871.

Keying the check on the property name left two ways through. A class
declaring the indexed pair getItem(int)/setItem(int, String) may also
declare an unrelated getItem(String) overload, and a one-argument call
dispatches to that overload, because the argument types choose the method
and the caller chooses the arguments. And the check was direction-agnostic,
so a read-only getItem(int) legitimised an unrelated two-argument
setItem(String, String). Both executed while method execution was denied.

The descriptor's own indexed accessor must now be the method that will
actually run: same name, same direction, and no same-arity overload for the
dispatcher to prefer instead.

The deny check is hoisted ahead of the indexed-property block, which it now
guards. The two are equivalent - with execution permitted, both paths ended
in the same call - but this way the introspection is skipped entirely on the
common path, and the block reads as the exception it is.

Also reword the deprecation javadoc, which claimed the key had never had any
effect: application code that sets it does still suppress the fast path.
Suppress the removal warning at the framework's own read of it.

Tests: an overload of an indexed accessor, and an unrelated setter named
after a read-only indexed property, are both blocked while execution is
denied. Both fail against the previous predicate. A read-only int-indexed
getter is added because it is the only shape that reaches
INDEXED_PROPERTY_INT - OGNL reclassifies a get/set pair as _OBJECT - so the
existing tests never covered that branch.

Full core suite: 3205 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lukaszlenart
lukaszlenart force-pushed the feature/WW-5697-indexed-access-fast-path branch from a701c00 to 8fc78ce Compare August 27, 2026 16:40
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
79.2% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

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.

2 participants