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

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

                Author: ASF GitHub Bot
            Created on: 05/Oct/26 19:20
            Start Date: 05/Oct/26 19:20
    Worklog Time Spent: 10m 
      Work Description: lukaszlenart commented on code in PR #1991:
URL: https://github.com/apache/struts/pull/1991#discussion_r4187837147


##########
core/src/test/java/org/apache/struts2/interceptor/AliasInterceptorTest.java:
##########
@@ -132,6 +138,42 @@ private SimpleAction executeWithBothNamesSubmitted(String 
actionName) throws Exc
         return (SimpleAction) proxy.getAction();
     }
 
+    // An alias target that is not annotated with @StrutsParameter must not be 
set when the source value

Review Comment:
   The test name and its assertions already say what this comment says. Could 
it go?
   
   Please also add a case for the stack-to-stack branch with 
`struts.chaining.requireAnnotations=true` (unannotated target stays untouched) 
next to the existing one with it off.



##########
core/src/main/java/org/apache/struts2/interceptor/AliasInterceptor.java:
##########
@@ -189,17 +198,31 @@ public void setAliasesKey(String aliasesKey) {
                         continue;
                     }
                     Evaluated value = new Evaluated(stack.findValue(name));
+                    boolean fromRawRequestParameter = false;
                     if (!value.isDefined()) {
-                        // workaround
+                        // workaround: name did not resolve on the stack (e.g. 
no earlier action in a

Review Comment:
   Could this go back to a one-line comment? Something like `// name did not 
resolve on the stack, fall back to the request parameter`. The comparison with 
`ChainingInterceptor` belongs in the PR description, where it already is.



##########
core/src/main/java/org/apache/struts2/interceptor/AliasInterceptor.java:
##########
@@ -189,17 +198,31 @@ public void setAliasesKey(String aliasesKey) {
                         continue;
                     }
                     Evaluated value = new Evaluated(stack.findValue(name));
+                    boolean fromRawRequestParameter = false;
                     if (!value.isDefined()) {
-                        // workaround
+                        // workaround: name did not resolve on the stack (e.g. 
no earlier action in a
+                        // chain set it), so fall back to the raw HTTP 
parameter of the same name - this
+                        // is the one path through which attacker-controlled, 
never-yet-authorized data
+                        // reaches newStack.setValue below, so it is the one 
path @StrutsParameter has to
+                        // gate. A name that DID resolve on the stack names 
something already on the
+                        // stack - typically state an earlier, 
properly-authorized bind already produced -
+                        // and copying that between properties is the same 
category of operation
+                        // ChainingInterceptor performs without requiring 
annotations by default.
                         HttpParameters contextParameters = 
ActionContext.getContext().getParameters();
 
                         if (null != contextParameters) {
                             Parameter param = contextParameters.get(name);
                             if (param.isDefined()) {
                                 value = new Evaluated(param.getValue());
+                                fromRawRequestParameter = true;
                             }
                         }
                     }
+                    if (fromRawRequestParameter && 
!parameterAuthorizer.isAuthorized(alias, authorizationTarget, action)) {

Review Comment:
   This is where I'd expect the second branch: when the value came from the 
stack, check it the same way, gated on `struts.chaining.requireAnnotations` 
(injected like `ChainingInterceptor.setRequireAnnotations`, `required = 
false`). With the chaining flag off (the default), behaviour stays exactly as 
in this PR.



##########
core/src/test/java/org/apache/struts2/interceptor/AliasInterceptorTest.java:
##########
@@ -132,6 +138,42 @@ private SimpleAction executeWithBothNamesSubmitted(String 
actionName) throws Exc
         return (SimpleAction) proxy.getAction();
     }
 
+    // An alias target that is not annotated with @StrutsParameter must not be 
set when the source value
+    // comes from AliasInterceptor's documented fallback to the raw HTTP 
parameter of the same name (used
+    // when the source name does not resolve on the stack) - that is the one 
path through which
+    // attacker-controlled, never-yet-authorized data reaches the target. An 
annotated target in the same
+    // aliases map, read through the exact same fallback, must still bind 
normally. A THIRD, unannotated
+    // target whose source name resolves directly on the stack (no HTTP 
parameter involved at all, the
+    // same category of copy ChainingInterceptor performs without an 
annotation requirement by default)
+    // must also still bind normally - the check must not reach that path.
+    public void testUnannotatedAliasTargetIsRejected() throws Exception {
+        Map<String, Object> httpParams = new HashMap<>();
+        httpParams.put("rawSourceAnnotated", "allowed-value");
+        httpParams.put("rawSourceUnannotated", "PWNED_VIA_ALIAS");

Review Comment:
   Please use a plain value here, e.g. `"value-from-request"`, to match the 
other fixtures in this class.



##########
core/src/test/java/org/apache/struts2/interceptor/AliasInterceptorTest.java:
##########
@@ -132,6 +138,42 @@ private SimpleAction executeWithBothNamesSubmitted(String 
actionName) throws Exc
         return (SimpleAction) proxy.getAction();
     }
 
+    // An alias target that is not annotated with @StrutsParameter must not be 
set when the source value
+    // comes from AliasInterceptor's documented fallback to the raw HTTP 
parameter of the same name (used
+    // when the source name does not resolve on the stack) - that is the one 
path through which
+    // attacker-controlled, never-yet-authorized data reaches the target. An 
annotated target in the same
+    // aliases map, read through the exact same fallback, must still bind 
normally. A THIRD, unannotated
+    // target whose source name resolves directly on the stack (no HTTP 
parameter involved at all, the
+    // same category of copy ChainingInterceptor performs without an 
annotation requirement by default)
+    // must also still bind normally - the check must not reach that path.
+    public void testUnannotatedAliasTargetIsRejected() throws Exception {
+        Map<String, Object> httpParams = new HashMap<>();
+        httpParams.put("rawSourceAnnotated", "allowed-value");
+        httpParams.put("rawSourceUnannotated", "PWNED_VIA_ALIAS");
+        ActionContext extraContext = 
ActionContext.of().withParameters(HttpParameters.create(httpParams).build());
+
+        XmlConfigurationProvider provider = new 
StrutsXmlConfigurationProvider("struts-alias-authorization.xml");
+        container.inject(provider);
+        // loadConfigurationProviders tears down and rebuilds the whole 
configuration on every call, so the

Review Comment:
   This one is worth keeping, since the ordering trap isn't obvious. A single 
line is enough, though.



##########
core/src/main/java/org/apache/struts2/interceptor/AliasInterceptor.java:
##########
@@ -189,17 +198,31 @@ public void setAliasesKey(String aliasesKey) {
                         continue;
                     }
                     Evaluated value = new Evaluated(stack.findValue(name));
+                    boolean fromRawRequestParameter = false;
                     if (!value.isDefined()) {
-                        // workaround
+                        // workaround: name did not resolve on the stack (e.g. 
no earlier action in a
+                        // chain set it), so fall back to the raw HTTP 
parameter of the same name - this
+                        // is the one path through which attacker-controlled, 
never-yet-authorized data
+                        // reaches newStack.setValue below, so it is the one 
path @StrutsParameter has to
+                        // gate. A name that DID resolve on the stack names 
something already on the
+                        // stack - typically state an earlier, 
properly-authorized bind already produced -
+                        // and copying that between properties is the same 
category of operation
+                        // ChainingInterceptor performs without requiring 
annotations by default.
                         HttpParameters contextParameters = 
ActionContext.getContext().getParameters();
 
                         if (null != contextParameters) {
                             Parameter param = contextParameters.get(name);
                             if (param.isDefined()) {
                                 value = new Evaluated(param.getValue());
+                                fromRawRequestParameter = true;
                             }
                         }
                     }
+                    if (fromRawRequestParameter && 
!parameterAuthorizer.isAuthorized(alias, authorizationTarget, action)) {
+                        LOG.debug("Alias target [{}] rejected by 
@StrutsParameter authorization on target [{}]",

Review Comment:
   `LOG.warn` here, please, and use the wording `ChainingInterceptor` uses, 
e.g. `Alias: property [{}] not set on [{}] because it is not annotated with 
@StrutsParameter`.



##########
core/src/test/resources/struts-alias-authorization.xml:
##########
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+-->
+<!DOCTYPE struts PUBLIC
+        "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
+        "https://struts.apache.org/dtds/struts-6.0.dtd";>
+<struts>
+    <include file="xwork-test-default.xml"/>
+    <package name="alias-authorization" extends="xwork-test-default">
+
+        <!

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

    Worklog Id:     (was: 1045813)
    Time Spent: 20m  (was: 10m)

> Enforce @StrutsParameter authorization in AliasInterceptor's raw-parameter 
> fallback
> -----------------------------------------------------------------------------------
>
>                 Key: WW-5758
>                 URL: https://issues.apache.org/jira/browse/WW-5758
>             Project: Struts 2
>          Issue Type: Bug
>          Components: Core
>    Affects Versions: 7.0.0, 7.3.0, 7.4.0
>            Reporter: Gouri Sankar A
>            Priority: Major
>             Fix For: 7.5.0
>
>          Time Spent: 20m
>  Remaining Estimate: 0h
>
> AliasInterceptor copies a value from one named source to a developer-chosen 
> target property, falling
> back to the raw HTTP request parameter of the source name when the source 
> does not resolve on the stack
> (its own documented behavior, shown in its own javadoc example). That 
> fallback value was never checked
> against @StrutsParameter before being written to the target, unlike every 
> other binding path in the
> framework: ParametersInterceptor, ChainingInterceptor, CookieInterceptor, and 
> the REST/JSON plugins (my
> own earlier finding, WW-5642, already merged) all call into the shared 
> ParameterAuthorizer before
> binding. AliasInterceptor never did. alias is in defaultStack, so it runs on 
> every request to any
> default-configured action.
> Tested on main at a235259 (7.5.0-SNAPSHOT), embedded Tomcat 10.1.34, stock 
> struts-default, an action with
> one @StrutsParameter-annotated property and one not. Binding the unannotated 
> property directly
> (?secret=DIRECT_ATTEMPT) is correctly rejected, stays untouched. The same 
> property, reached by
> configuring aliases to map a raw HTTP parameter name onto it and sending that 
> parameter instead
> (?rawParam=PWNED_VIA_ALIAS), is not rejected and takes the attacker supplied 
> value directly.
> The fix is deliberately narrower than checking every alias target. 
> AliasInterceptor has two different
> value sources: a name that resolves directly on the stack, which names state 
> an earlier, properly
> authorized bind already produced such as an earlier action in a chain, and 
> the raw HTTP parameter
> fallback, which is the only path that carries never authorized external 
> input. Checking every target
> unconditionally would have also blocked the first, legitimate category. I 
> built that broader version
> first, live tested it, found it silently broke a same request stack to stack 
> copy with no HTTP parameter
> involved at all, and narrowed the check to apply only when the value came 
> from the fallback, matching
> exactly what ChainingInterceptor already exempts by default for the same 
> underlying reason.
> Affects 7.0.0 through 7.4.0, verified directly on all three released versions 
> by reading the code at each
> tag. requireAnnotations has been the default since the very first 7.0 release 
> and AliasInterceptor has
> never checked it. Does not affect 6.x: requireAnnotations is not set in 6.x's 
> default.properties at all,
> confirmed directly on 6.12.0, so there is no default on control for any 
> interceptor to bypass on that
> line.
> This is the same bug class as WW-5642, already merged: a secondary binding 
> path that does not enforce
> @StrutsParameter the way ParametersInterceptor does.
> I have a patch with a regression test ready and will open a PR referencing 
> this ticket.



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

Reply via email to