WW-5697 Restrict the indexed-access fast path in XWorkMethodAccessor to real indexed properties - #1871
WW-5697 Restrict the indexed-access fast path in XWorkMethodAccessor to real indexed properties#1871lukaszlenart wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
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.
…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>
a701c00 to
8fc78ce
Compare
SonarCloud failed the quality gate on PR #1871 at 79.17% new-code coverage. The uncovered branches were all real behaviour that nothing asserted: - the deprecated DENY_INDEXED_ACCESS_EXECUTION key, both set and set to false. Its javadoc claims application code can still suppress the exemption with it, which is the reason it was deprecated rather than removed, and nothing tested that claim. - the object-indexed mutator half of the pair, which parameter binding itself walks through. - methods carrying neither prefix, which never reach the property lookup. - an overload of another arity, which cannot be dispatched to and so must not cost the bean its indexed property access. Each new test was checked by mutation: making the predicate always true, never honouring the legacy key, always honouring it, and dropping the argument-count filter each fail exactly the tests that assert that branch. New-code coverage goes from 79.17% to roughly 92%. What stays uncovered is defensive only: the null target, the OgnlException catch, and the two guards against a descriptor accessor that disagrees with the invoked method. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9Pjt4rvb1ASASjSTHsUhL
…fixtures The gate passes, but four issues stand, all in the test beans. S4144 is the one worth having: attack(String) and getAttack(String) had identical bodies because both recorded into attackArgument, so neither of the tests asserting on that field could tell which of the two methods had run. The unprefixed pair now records into its own field, which is what the tests naming it actually mean to assert. The three S1172s are unused second parameters on fixtures whose two-argument shape is the whole point, so the parameter cannot be removed. Each now records the full call instead of only its first argument, which is what the surrounding fixtures already did and costs nothing. Mutation still holds: forcing isIndexedPropertyAccessor to accept everything fails all six "blocks" tests, the two switched to the new field included. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9Pjt4rvb1ASASjSTHsUhL
|



Fixes WW-5697
Problem
XWorkMethodAccessor.callMethod(...)skipped thedenyMethodExecutioncheck for any method whose name began withgetand took one argument, orsetand took two: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 ofgetSomething('value').propertycallsgetSomething("value").The flag the branch consults,
DENY_INDEXED_ACCESS_EXECUTION, is never written anywhere in main source, soexecis alwaysnulland 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:
getItem(int)/setItem(int, String)may also declare an unrelatedgetItem(String)overload, and a one-argument call dispatches to that overload — the argument types choose the method, and the caller chooses the arguments;getItem(int)legitimises an unrelated two-argumentsetItem(String, String).So the descriptor's own indexed accessor must be the method that will actually run: same name, same direction (
getIndexedReadMethod/getIndexedWriteMethod, for bothIndexedPropertyDescriptorand OGNL'sObjectIndexedPropertyDescriptor), and no same-arity overload for the dispatcher to prefer instead. Everything else falls through to thedenyMethodExecutioncheck.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
callMethodWithDebugInfocall — but this way the introspection is skipped entirely on the common path, and the block reads as the exception it is.DENY_INDEXED_ACCESS_EXECUTIONis 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,AliasInterceptorandStaticParametersInterceptor— 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:
String getKeyed(String)with no matchingsetKeyed(String, String)reportsINDEXED_PROPERTY_NONE, because OGNL'sfindObjectIndexedPropertyDescriptorsrequires an exact pair with matching key and value types. The same applies when the types do not line up, e.g.String getKeyed(String)againstvoid setKeyed(String, Object). OGNL itself refusesbean.keyed['k']for such beans, so this is consistent, but it is a change.get(...)/set(...)are no longer exempt."get".startsWith("get")matched the old fast path, somyMap.get('k').fieldbound underdenyMethodExecution. There is no property name left once the prefix is stripped, so it cannot be an indexed accessor.Tests
XWorkMethodAccessorTestis new and covers eight behaviours, including:get(String)is not executed while deniedNote that a
getItem(int)/setItem(int, String)pair reports asINDEXED_PROPERTY_OBJECT, not_INT:findObjectIndexedPropertyDescriptorsoverwrites thejava.beansdescriptor whenever it finds a matching pair. A read-only int-indexed getter is the only shape that reaches the_INTbranch, 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
coresuite green: 3205 tests, 0 failures, 0 errors.Related
WW-5698 covers a separate issue found alongside this one — the
ModelDrivenexemption inStrutsParameterAuthorizeralso exempting the action's own members. Different cause, different fix, not addressed here. An end-to-endModelDrivenbinding 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