[ 
https://issues.apache.org/jira/browse/WW-5624?focusedWorklogId=1014514&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-1014514
 ]

ASF GitHub Bot logged work on WW-5624:
--------------------------------------

                Author: ASF GitHub Bot
            Created on: 10/Apr/26 11:54
            Start Date: 10/Apr/26 11:54
    Worklog Time Spent: 10m 
      Work Description: tranquac commented on PR #1657:
URL: https://github.com/apache/struts/pull/1657#issuecomment-4223563657

   Thanks for the thorough review @lukaszlenart! I've pushed a new commit 
([aa78085](https://github.com/tranquac/struts/commit/aa78085)) addressing all 5 
blocking issues plus the inline suggestion. Here's a point-by-point summary:
   
   ---
   
   ### 1. Rename `DefaultParameterAuthorizer` → `StrutsParameterAuthorizer` ✅
   
   Done — class renamed, all references updated (`struts-beans.xml`, 
`DefaultConfiguration.java`, both test files). Git detected it as a rename (93% 
similarity).
   
   ---
   
   ### 2. ModelDriven exemption too broad ✅
   
   Changed the guard in `StrutsParameterAuthorizer.isAuthorized()` from:
   ```java
   if (target != action) { return true; }
   ```
   to:
   ```java
   if (target != action && action instanceof ModelDriven) { return true; }
   ```
   Now only actions that explicitly implement `ModelDriven` will have their 
model exempted. If a `JSONInterceptor` `root` configuration resolves to a 
non-action object on a non-ModelDriven action, annotation checks are still 
enforced. Added a regression test 
`nonModelDrivenAction_differentTarget_notExempt()` to lock this in.
   
   ---
   
   ### 3. Nested enforcement missing in `JSONInterceptor` ✅
   
   `filterUnauthorizedKeys()` now delegates to a recursive 
`filterUnauthorizedKeysRecursive()` that builds dot-notation paths 
(`"address.city"`) as it descends into nested Maps and Lists:
   
   ```java
   String fullPath = prefix.isEmpty() ? key : prefix + "." + key;
   if (!parameterAuthorizer.isAuthorized(fullPath, target, action)) { 
it.remove(); }
   // recurse into nested Map / List
   ```
   
   This reuses the existing `@StrutsParameter(depth=N)` depth-counting in 
`StrutsParameterAuthorizer` — a nested key `"address.city"` has depth=1, which 
requires `getAddress()` to carry `@StrutsParameter(depth≥1)`. Added regression 
tests for nested key filtering and for the non-action root object case.
   
   ---
   
   ### 4. REST shallow copy problem ✅
   
   `copyAuthorizedProperties()` is now recursive. For each property, if the 
value is a *complex bean type* (not primitive, String, Number, Date, Temporal, 
Collection, Map, array, enum), it creates a fresh target instance and recurses 
with the extended path prefix:
   
   ```java
   if (isNestedBeanType(sourceValue.getClass())) {
       copyAuthorizedProperties(sourceValue, targetValue, action, fullPath); // 
depth-aware recurse
   } else {
       writeMethod.invoke(target, sourceValue);
   }
   ```
   
   Collections and Maps are copied as-is — their contents were already 
deserialized and the depth check on the parent property is sufficient.
   
   ---
   
   ### 5. Null-skipping semantics — kept intentionally, now documented ✅
   
   After careful consideration: in two-phase deserialization, `null` in 
`freshInstance` is **ambiguous** — it could mean "field was explicitly set to 
null in the request body" or "field was not present at all". Copying null would 
silently wipe pre-initialized fields on the target for every property not sent 
by the client.
   
   The safer default is to skip null, and I've added an inline comment 
explaining the reasoning:
   ```java
   // Intentionally skip null values: in two-phase deserialization, properties 
NOT present in the
   // request body will be null in the fresh instance. Copying null would clear 
pre-initialized
   // fields on the target. This is the safer default.
   ```
   
   If you feel strongly that explicit null should be propagated, I can add a 
`clearMissingFields` flag (default `false`) — but I wanted your input before 
adding API surface.
   
   ---
   
   ### 6. No-arg constructor requirement ✅
   
   Added a `createFreshInstance()` helper that catches 
`ReflectiveOperationException` and returns `null` on failure. The interceptor 
now has two paths:
   
   - **Two-phase (preferred)**: deserialize into `freshInstance`, then 
`copyAuthorizedProperties()` — requires no-arg constructor
   - **Single-phase fallback**: deserialize directly into `target`, then 
`scrubUnauthorizedProperties()` nulls out any unauthorized property — no 
constructor requirement
   
   The scrub fallback is logged at `WARN` so operators know the less-safe path 
is active. Backward compat fully preserved.
   
   ---
   
   ### Test results
   
   ```
   core:         2920 tests — 0 failures, 0 errors
   plugins/json:  124 tests — 0 failures, 0 errors  
   plugins/rest:   76 tests — 0 failures, 0 errors
   ```
   
   New regression tests added:
   - `ParameterAuthorizerTest#nonModelDrivenAction_differentTarget_notExempt`
   - `JSONInterceptorTest#testNestedJsonKeysRecursivelyFiltered`
   - `JSONInterceptorTest#testNonActionRootObjectStillChecked`
   
   Please let me know if anything needs further adjustment!




Issue Time Tracking
-------------------

    Worklog Id:     (was: 1014514)
    Time Spent: 1h 50m  (was: 1h 40m)

> Request body population bypasses @StrutsParameter contract outside 
> ParametersInterceptor
> ----------------------------------------------------------------------------------------
>
>                 Key: WW-5624
>                 URL: https://issues.apache.org/jira/browse/WW-5624
>             Project: Struts 2
>          Issue Type: Bug
>          Components: Plugin - JSON, Plugin - REST
>    Affects Versions: 7.1.1
>            Reporter: Tran Quac
>            Priority: Major
>             Fix For: 7.2.0
>
>          Time Spent: 1h 50m
>  Remaining Estimate: 0h
>
> h2. Summary
> {{@StrutsParameter}} enforcement is currently implemented in 
> {{ParametersInterceptor}} for standard request parameter binding, but 
> request-body based binders in some plugins bypass that authorization model 
> and populate action/model objects directly.
> This creates inconsistent behavior between URL/form parameters and JSON/XML 
> request bodies, and may allow mass assignment of properties that would 
> normally be rejected by {{ParametersInterceptor}}.
> h2. Affected areas currently identified
> * JSON plugin:
> {{JSONPopulator.populateObject()}} sets properties via direct reflection and 
> does not follow the full {{@StrutsParameter}} authorization rules.
> * REST plugin:
> {{JacksonJsonHandler.toObject()}} updates target objects directly via Jackson 
> and does not follow the full {{@StrutsParameter}} authorization rules.
> h2. Problem scope
> The issue is broader than checking whether a setter is annotated. The current 
> core contract in {{ParametersInterceptor}} also includes:
> * permitted nesting depth
> * authorization based on the exposed root member
> * ModelDriven handling
> * transition mode semantics
> * related allowlisting behavior
> Any request-body binding implementation should align with that same contract, 
> otherwise Struts applies different security rules depending on how input 
> reaches the action/model.
> h2. Expected direction
> Instead of implementing separate partial checks in each plugin, Struts should 
> reuse or extract the shared parameter-binding authorization logic from 
> {{ParametersInterceptor}} and apply it consistently across request-body 
> binders.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to