github-advanced-security[bot] commented on code in PR #1872:
URL: https://github.com/apache/struts/pull/1872#discussion_r3878340751


##########
core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameterAuthorizer.java:
##########
@@ -115,28 +115,102 @@
 
         long paramDepth = parameterName.codePoints().mapToObj(c -> (char) 
c).filter(NESTING_CHARS::contains).count();
 
-        // ModelDriven exemption: only exempt when the action explicitly 
implements ModelDriven
-        // and the target is its model object. This prevents non-ModelDriven 
root objects
-        // (e.g. JSONInterceptor's configurable rootObject) from bypassing 
annotation checks.
-        if (target != action && action instanceof ModelDriven) {
-            LOG.debug("ModelDriven target detected (action implements 
ModelDriven), exempting from @StrutsParameter annotation requirement");
-            return true;
+        int nestingIndex = indexOfAny(parameterName, NESTING_CHARS_STR);
+        String rootProperty = nestingIndex == -1 ? parameterName : 
parameterName.substring(0, nestingIndex);
+        if (rootProperty.isEmpty()) {
+            LOG.debug("Parameter [{}] begins with a nesting character, so it 
names no root property to authorize; rejecting",
+                    parameterName);
+            return false;
         }
+        String normalisedRootProperty = 
Character.toLowerCase(rootProperty.charAt(0)) + rootProperty.substring(1);
 
-        // Transition mode: depth-0 (non-nested) parameters are exempt
+        // Transition mode: depth-0 (non-nested) parameters are exempt. 
Checked before the ModelDriven
+        // exemption so that it also covers a ModelDriven action's own 
members, which would otherwise
+        // have no migration path once the exemption is scoped to the model.
         if (requireAnnotationsTransitionMode && paramDepth == 0) {
             LOG.debug("Annotation transition mode enabled, exempting 
non-nested parameter [{}] from @StrutsParameter annotation requirement",
                     parameterName);
             return true;
         }
 
-        int nestingIndex = indexOfAny(parameterName, NESTING_CHARS_STR);
-        String rootProperty = nestingIndex == -1 ? parameterName : 
parameterName.substring(0, nestingIndex);
-        String normalisedRootProperty = 
Character.toLowerCase(rootProperty.charAt(0)) + rootProperty.substring(1);
+        // ModelDriven exemption: only exempt when the action explicitly 
implements ModelDriven
+        // and the target is its model object. This prevents non-ModelDriven 
root objects
+        // (e.g. JSONInterceptor's configurable rootObject) from bypassing 
annotation checks.
+        if (target != action && action instanceof ModelDriven) {
+            return isAuthorizedOnModelDrivenAction(normalisedRootProperty, 
target, action, paramDepth);
+        }
 
         return hasValidAnnotatedMember(normalisedRootProperty, target, 
paramDepth);
     }
 
+    /**
+     * Decides authorization for a {@link ModelDriven} action, whose model is 
on top of the value stack.
+     * <p>
+     * Returning an object from {@code getModel()} declares that object to be 
request surface, so anything the
+     * model itself can take is exempt from the {@link StrutsParameter} 
requirement. The exemption stops there:
+     * OGNL resolves the parameter name against the whole stack, which also 
holds the action, so a property
+     * declared on the action is still subject to the annotation requirement. 
Without that distinction a
+     * ModelDriven action would silently expose its own members.
+     * <p>
+     * A property declared on neither is allowed, since it cannot be reaching 
a member of the action - typically
+     * it is bound by a custom OGNL property accessor on the model, such as a 
Map-backed model.
+     */
+    protected boolean isAuthorizedOnModelDrivenAction(String rootProperty, 
Object model, Object action, long paramDepth) {
+        if (declaresProperty(model, rootProperty, paramDepth)) {
+            LOG.debug("Property [{}] belongs to the ModelDriven model, 
exempting from @StrutsParameter annotation requirement",
+                    rootProperty);
+            return true;
+        }
+        if (!declaresProperty(action, rootProperty, paramDepth)) {
+            LOG.debug("Property [{}] is declared on neither the model nor the 
action, exempting from @StrutsParameter annotation requirement",
+                    rootProperty);
+            return true;
+        }
+        LOG.debug("Property [{}] is declared on the ModelDriven action itself, 
applying the @StrutsParameter annotation requirement",
+                rootProperty);
+        return hasValidAnnotatedMember(rootProperty, action, paramDepth);
+    }
+
+    /**
+     * Whether {@code target} can itself take {@code property} at this depth - 
as a bean property whose relevant
+     * accessor exists, the setter for a depth-0 parameter and the getter for 
a nested one, or as a public instance
+     * field. Any {@link StrutsParameter} annotation is irrelevant here; this 
asks only what the object can absorb.
+     * <p>
+     * It has to be bindability rather than the name alone, because OGNL walks 
the stack until an object actually
+     * accepts the assignment. A model which merely names the property without 
being able to take it - a getter-only
+     * property under a depth-0 parameter, say - does not absorb that 
parameter: OGNL moves on to the action, and an
+     * exemption granted on the name alone would hand over the action's own 
member, which is the very thing this
+     * scoping exists to prevent. Inherited public fields count for the same 
reason, that OGNL can set them.
+     */
+    protected boolean declaresProperty(Object target, String property, long 
paramDepth) {
+        BeanInfo beanInfo = getBeanInfo(target);
+        if (beanInfo != null && 
Arrays.stream(beanInfo.getPropertyDescriptors())
+                .filter(desc -> desc.getName().equals(property))
+                .anyMatch(desc -> (paramDepth == 0 ? desc.getWriteMethod() : 
desc.getReadMethod()) != null)) {
+            return true;
+        }
+        return declaresBindablePublicField(target, property, paramDepth);
+    }
+
+    /**
+     * Whether {@code target} exposes {@code property} as a public instance 
field that this parameter could bind
+     * through. {@link Class#getField} rather than {@code getDeclaredField}, 
since an inherited public field is just
+     * as settable as a declared one. Static fields are not per-instance 
request surface, and a final field cannot
+     * take a depth-0 assignment, so neither counts as absorbing the parameter.
+     */
+    protected boolean declaresBindablePublicField(Object target, String 
property, long paramDepth) {
+        Field field;
+        try {
+            field = ultimateClass(target).getField(property);

Review Comment:
   ## SonarCloud / Reflection should not be vulnerable to injection attacks
   
   <!--SONAR_ISSUE_KEY:AaBHAzt2KNzgUaIETn2l-->Change this code to not construct 
class or method names directly from user-controlled data. <p>See more on <a 
href="https://sonarcloud.io/project/issues?id=apache_struts&issues=AaBHAzt2KNzgUaIETn2l&open=AaBHAzt2KNzgUaIETn2l&pullRequest=1872";>SonarQube
 Cloud</a></p>
   
   [Show more 
details](https://github.com/apache/struts/security/code-scanning/1438)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to