Conversation
…raw-parameter fallback AliasInterceptor copies a value from one named stack/request slot to another, falling back to the raw HTTP parameter of the source name when the source does not resolve on the stack (its own documented behavior). That fallback value was never checked against @StrutsParameter before being written to the alias target, unlike every other binding path in the framework (ParametersInterceptor, ChainingInterceptor with struts.chaining.requireAnnotations, CookieInterceptor, the REST/JSON plugins): an alias target that is not annotated could still be set, with the value sourced directly from an attacker-controlled request parameter when the fallback fires. Fix: inject the same shared ParameterAuthorizer used by ParametersInterceptor and CookieInterceptor, and check it for the alias target, but only when the value actually came from the raw-parameter fallback - not when the source name resolved directly on the stack. A name that resolves on the stack names something an earlier, properly-authorized bind already produced (e.g. an earlier action in a chain); copying that between properties is the same category of operation ChainingInterceptor performs without requiring annotations by default, and must stay unaffected. No new configuration flag: this follows the same struts.parameters.requireAnnotations default (true since 7.0) the rest of the framework already uses. Affects 7.0.0 through 7.4.0 (verified directly on 7.0.0, 7.3.0, 7.4.0): struts.parameters.requireAnnotations=true has been the default since the 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 (falls back to the Java field default of false), so there is no default-on control to bypass there.
lukaszlenart
left a comment
There was a problem hiding this comment.
Thanks for this. I checked it out and ran it: AliasInterceptorTest passes with the change, and the new test fails on main. Resolving the target through parameterAuthorizer.resolveTarget(action) is right here: the alias writes through the value stack, just like ParametersInterceptor, so on a ModelDriven action the model gets the value.
Before this can be merged I'd like it to line up with how ChainingInterceptor handles the same question (WW-5631):
- The stack-to-stack branch should follow
struts.chaining.requireAnnotations. Copying a value that already resolves on the stack is the chaining case, and aliasing is documented mainly as glue for action chaining.ChainingInterceptorleaves that copy unchecked by default but lets an application switch the check on. The PR exempts the branch unconditionally, so an application that has setstruts.chaining.requireAnnotations=truewould still get unannotated properties filled through an alias on a chained action. Gating the request-parameter branch onstruts.parameters.requireAnnotations(as now) and the stack branch onstruts.chaining.requireAnnotationscopies the split the framework already has, with no new constant. - Log a skipped alias at
WARN, notdebug. For existing applications this changes behaviour: the documented#{ 'foo' : 'bar' }example stops fillingbarunlessbaris annotated.ChainingInterceptorwarns when it skips a property (ChainingInterceptor.java:250). Adebugline means people find out from missing values, not from the log. - Documentation. This needs an update to the Alias Interceptor page, an entry for the alias interceptor in the "Where authorization applies" list on the
@StrutsParameterpage, and a note thatstruts.parameters.requireAnnotations.transitionModeexempts non-nested alias targets while an application migrates. I'll take care of the Version Notes entry. If you'd like to open the struts-site PR, that would be welcome. - Comments. The reasoning in the inline comments is already in the PR description. Please trim the in-file comments to what a reader can't see from the code (details inline).
| boolean fromRawRequestParameter = false; | ||
| if (!value.isDefined()) { | ||
| // workaround | ||
| // workaround: name did not resolve on the stack (e.g. no earlier action in a |
There was a problem hiding this 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.
| } | ||
| } | ||
| } | ||
| if (fromRawRequestParameter && !parameterAuthorizer.isAuthorized(alias, authorizationTarget, action)) { |
There was a problem hiding this 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.
| } | ||
| } | ||
| if (fromRawRequestParameter && !parameterAuthorizer.isAuthorized(alias, authorizationTarget, action)) { | ||
| LOG.debug("Alias target [{}] rejected by @StrutsParameter authorization on target [{}]", |
There was a problem hiding this 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.
| return (SimpleAction) proxy.getAction(); | ||
| } | ||
|
|
||
| // An alias target that is not annotated with @StrutsParameter must not be set when the source value |
There was a problem hiding this 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.
| public void testUnannotatedAliasTargetIsRejected() throws Exception { | ||
| Map<String, Object> httpParams = new HashMap<>(); | ||
| httpParams.put("rawSourceAnnotated", "allowed-value"); | ||
| httpParams.put("rawSourceUnannotated", "PWNED_VIA_ALIAS"); |
There was a problem hiding this comment.
Please use a plain value here, e.g. "value-from-request", to match the other fixtures in this class.
|
|
||
| XmlConfigurationProvider provider = new StrutsXmlConfigurationProvider("struts-alias-authorization.xml"); | ||
| container.inject(provider); | ||
| // loadConfigurationProviders tears down and rebuilds the whole configuration on every call, so the |
There was a problem hiding this comment.
This one is worth keeping, since the ordering trap isn't obvious. A single line is enough, though.
|
|
||
| private String annotatedTarget; | ||
|
|
||
| // Deliberately NOT annotated with @StrutsParameter: an alias may not use this as a copy target |
There was a problem hiding this comment.
Can the field comments here go? Each field's role is clear from its name and the test that uses it.
| <include file="xwork-test-default.xml"/> | ||
| <package name="alias-authorization" extends="xwork-test-default"> | ||
|
|
||
| <!-- Neither raw source name resolves on the stack, forcing AliasInterceptor's documented |
There was a problem hiding this comment.
Same here: the action name and aliases make the fixture's purpose clear, so the comment block can go.
Addresses review feedback from lukaszlenart on PR apache#1991 / WW-5758: 1. The stack-to-stack branch now follows struts.chaining.requireAnnotations (default false, matching WW-5631's ChainingInterceptor precedent) instead of being unconditionally exempt. An application that has turned struts.chaining.requireAnnotations on now gets the same enforcement on an aliased copy of an already-resolved value that it gets from chaining. 2. Rejections now log at WARN, matching ChainingInterceptor.java:250, since this is a behavior change for existing applications using the documented aliases example. 3. Trimmed the in-file comment to the one thing not visible from the code (the WW-5631 split); the full reasoning is in the PR description.
|
Thanks for the detailed review. 1 and 2 addressed: the stack-resolved branch now follows struts.chaining.requireAnnotations (default false, same as ChainingInterceptor per WW-5631), and rejections log at WARN matching ChainingInterceptor.java:250's message format. New test (testUnannotatedStackToStackAliasTargetIsRejectedWhenChainingAnnotationsRequired) proves the gate. 4 addressed: trimmed the in-file comment to the WW-5631 cross-reference; the rest of the reasoning is in the PR description. 3 (docs): happy to open the struts-site PR if that's still wanted — let me know. |
|
Thanks, the split looks right now. I ran the tests at 36ddb05 and all 12 pass. If I drop A few things are left:
On the docs: yes, please go ahead with the struts-site PR. It should cover the Alias Interceptor page, a line for the alias interceptor in the "Where authorization applies" list on the |
Addresses the rest of lukaszlenart's review on PR apache#1991 / WW-5758: - Fix: the @StrutsParameter check now runs only inside the value.isDefined() branch, so an alias whose source never resolves at all (neither on the stack nor as a request parameter) is no longer spuriously warned about and rejected - there was nothing to copy either way. - WARN wording now matches his suggested text exactly ('not set on', not 'not copied to'). - Trimmed every flagged comment: the production fallback comment back to one line, the WW-5631 cross-reference to one line above the check, removed the two test-method comment blocks (name and assertions already say what they said), removed the field comments in AliasAuthorizationTestAction (clear from field names and the test), removed the XML fixture's comment block (clear from the action name and aliases). - Changed the HTTP parameter value in the first test from 'PWNED_VIA_ALIAS' to the plain 'value-from-request', matching the other fixtures in this class. - setRequireChainingAnnotations now uses BooleanUtils.toBoolean, matching ChainingInterceptor.setRequireAnnotations exactly. - default.properties: struts.chaining.requireAnnotations now mentions AliasInterceptor alongside ChainingInterceptor. - New test (testNoWarningWhenAliasSourceNeverResolvesAtAll, using LogCapture) proves the spurious- warning bug is fixed; red/green proven by temporarily reverting the check's position.
…ptor-authorization
|
Good catch on the spurious warning — fixed, and thanks for pointing out I'd missed the inline comments from the first review (I'd only checked the top-level review body, not the line comments; fixed my process for next time).
Also merged in On the struts-site PR: happy to open it, covering the Alias Interceptor page, the |
|
Docs are up: apache/struts-site#347 |
Fixes WW-5758
AliasInterceptorcopies 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). That fallback value was never checked against@StrutsParameter, unlike every other binding path (ParametersInterceptor,ChainingInterceptor,CookieInterceptor, the REST/JSON plugins): an alias target that is not annotated could still be set, with the value sourced directly from an attacker-controlled request parameter when the fallback fires.Injects the same shared
ParameterAuthorizerParametersInterceptorandCookieInterceptoralready use, and checks it for the alias target, but only when its value came from the raw-parameter fallback. A source name that resolves directly on the stack names something an earlier, properly-authorized bind already produced; copying that between properties is the same category of operationChainingInterceptoralready performs without requiring annotations by default, and stays unaffected. No new configuration flag: this follows the samestruts.parameters.requireAnnotationsdefault (true since 7.0) the rest of the framework already uses.An annotated alias target, reached through the same fallback, is unaffected.
One regression test fails without the change:
AliasInterceptorTest#testUnannotatedAliasTargetIsRejected, using a dedicated test action and XML fixture kept separate from the widely-sharedSimpleActionfixture so it cannot affect, or be affected by, the other suites that reuse it. It proves all three cases together in one request: an unannotated target via the fallback is rejected, an annotated target via the same fallback still binds, and an unannotated target reached via a stack-resolved name (no fallback) still binds.