[
https://issues.apache.org/jira/browse/WW-5761?focusedWorklogId=1052405&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-1052405
]
ASF GitHub Bot logged work on WW-5761:
--------------------------------------
Author: ASF GitHub Bot
Created on: 04/Nov/26 11:04
Start Date: 04/Nov/26 11:04
Worklog Time Spent: 10m
Work Description: emilhuseyn opened a new pull request, #2109:
URL: https://github.com/apache/struts/pull/2109
Fixes [WW-5761](https://issues.apache.org/jira/browse/WW-5761)
`StrutsParameterAuthorizer.isAuthorized` runs before OGNL and has to predict
which accessor or field a parameter name will reach. If that prediction and
OGNL's own name resolution ever differ, the value goes through a member nobody
checked. This adds a second check at the point where OGNL resolves the member,
behind a new constant that is off by default:
```
struts.parameters.requireAnnotations.enforceOnResolvedMember=false
```
With the flag off nothing changes: `ParametersInterceptor` sets no guard and
binding takes the same path as today.
With the flag on (and `struts.parameters.requireAnnotations` on),
`ParametersInterceptor` asks the authorizer for a `ParameterMemberGuard` for
each parameter and sets it on the binding stack's `SecurityMemberAccess` just
before `stack.setParameter`. It is removed when the loop ends.
`SecurityMemberAccess.isAccessible` asks the guard about every member OGNL
resolves. As you pointed out, that bean is `scope="prototype"` and every value
stack has its own instance, so the guard is never shared between requests.
What the guard checks:
- Only members used on objects standing in the value stack root. A deeper
object is reached through a root member whose depth already covers it.
- The `ModelDriven` model is exempt by identity, as in the authorizer.
- A nested parameter whose root getter returned null makes the null handler
call the setter, which is usually unannotated. The guard allows that setter
only after the getter of the same property has passed on the same object.
- A member also passes if a same-named overload or a public field of that
property is annotated, which is what `isAuthorized` accepts today.
- Transition mode keeps depth 0 exempt.
Not changed: Map-backed models and custom `PropertyAccessor`s never reach
`isAccessible`, so the authorizer stays the gate there. `ParameterNameAware`,
the JSON and REST plugins and `CookieInterceptor` are untouched.
Given orphan-oss/ognl#755, the authorizer side is kept small.
`StrutsParameterAuthorizer` only gains `memberGuard(...)`, a factory, and the
depth count moves into a helper both methods use. The guard itself is the
package-private `StrutsParameterMemberGuard`. `ParameterAuthorizer.memberGuard`
is a default method returning `PERMIT_ALL`, so a custom authorizer compiles and
behaves as before.
Tests: `ParameterMemberGuardTest` binds through a real OGNL value stack with
no authorizer pre-check, so it shows the guard holding on its own: annotated
and unannotated setters, fluent setters, public fields, overloads, nested
depth, the null-handler case, `ModelDriven`, an object below the action,
transition mode, removal after binding, and the interceptor with an authorizer
whose prediction has drifted, rejected with the flag on and bound as before
with the flag off or without `requireAnnotations`.
`ParameterMemberGuardTest`, `ParameterAuthorizerTest`,
`ParametersInterceptorTest`, `StrutsParameterAnnotationTest` and
`SecurityMemberAccessTest` pass locally on JDK 21, along with the full `core`
suite. I have not benchmarked the flag-on cost: one annotation lookup per
member OGNL resolves on a root object, plus a scan of the public methods and
fields when that member itself is unannotated.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Issue Time Tracking
-------------------
Worklog Id: (was: 1052405)
Remaining Estimate: 0h
Time Spent: 10m
> Enforce @StrutsParameter on the member OGNL resolves during parameter binding
> -----------------------------------------------------------------------------
>
> Key: WW-5761
> URL: https://issues.apache.org/jira/browse/WW-5761
> Project: Struts 2
> Issue Type: Improvement
> Components: Core
> Reporter: Lukasz Lenart
> Priority: Major
> Fix For: 7.6.0
>
> Time Spent: 10m
> Remaining Estimate: 0h
>
> {{@StrutsParameter}} is enforced once, before OGNL runs:
> {{StrutsParameterAuthorizer}} takes the parameter name and predicts which
> accessor or field OGNL will go through, then checks that member for the
> annotation. {{SecurityMemberAccess}}, which OGNL consults for every member it
> actually touches, does not know about the annotation at all.
> Every time OGNL's resolution rules and the prediction drift apart, the
> authorizer has to be taught again:
> * WW-5709: fluent (non-void) setters, which {{java.beans}} does not treat as
> write methods but OGNL binds through
> * WW-5759: OGNL 3.4.14's JavaBeans-style accessor names ({{setuRange}} before
> {{setURange}}), and getters whose {{java.beans}} property name differs from
> the name OGNL reaches them by ({{getURL()}})
> The prediction is a hand-written copy of OGNL internals
> ({{accessorBaseNames}}, set/is/get order, overload choice), so the next OGNL
> change to name resolution needs the same follow-up.
> h3. Proposal
> Enforce the annotation where OGNL resolves the member, during parameter
> binding only.
> Binding already runs on its own value stack
> ({{ParametersInterceptor.toNewStack}}). Its {{SecurityMemberAccess}} already
> receives binding-only settings ({{useAcceptProperties}} /
> {{useExcludeProperties}} via {{MemberAccessValueStack}}). A binding mode on
> that instance could require {{@StrutsParameter}} on the actual
> {{Method}}/{{Field}} OGNL is about to use on the action, whatever name it was
> reached by. The authorizer's prediction would remain a fast pre-check and
> would no longer be the only gate.
> h3. Open design questions
> * *Depth*: {{isAccessible}} sees one member at a time, not its position in
> the expression. The required {{depth}} of the root getter is known only to
> the caller, so it has to be tracked per binding (e.g. the authorizer records
> the authorized root and depth in the binding context).
> * *ModelDriven*: model members are exempt, so {{SecurityMemberAccess}} needs
> to know which object is the model, by identity, as
> {{StrutsParameterAuthorizer.resolveTarget}} already does.
> * *Paths that never call {{isAccessible}}*: Map-backed models and custom
> {{PropertyAccessor}}s keep relying on the authorizer.
> * *Transition mode, {{ParameterNameAware}}, the JSON/REST deserialization
> paths* (WW-5624) and {{CookieInterceptor}} (WW-5627) share
> {{ParameterAuthorizer}} and need a consistent story.
> * *Cost*: an annotation lookup per member access during binding; cacheable
> per {{Member}}.
> Fits 8.0.0 together with the other {{SecurityMemberAccess}} changes (WW-5678)
> and the OGNL 3.5 upgrade (WW-5326).
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)