[ 
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)

Reply via email to