Repository navigation
WW-5758 Enforce @StrutsParameter authorization in AliasInterceptor's raw-parameter fallback - #1991
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).
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 |
lukaszlenart
left a comment
There was a problem hiding this comment.
Thanks for working through both rounds. I re-ran AliasInterceptorTest and ChainingInterceptorTest at c132367 and they pass. With the check moved back before isDefined(), testNoWarningWhenAliasSourceNeverResolvesAtAll fails, so it really checks the fix. Looks good to me.
* Document AliasInterceptor's @StrutsParameter authorization WW-5758 (apache/struts#1991) adds @StrutsParameter authorization to AliasInterceptor: its raw-request-parameter fallback follows struts.parameters.requireAnnotations like the Parameters Interceptor, and copying a value already resolved on the stack follows the opt-in struts.chaining.requireAnnotations like the Chaining Interceptor. Adds a Parameter Authorization section to the Alias Interceptor page, mirroring the Chaining Interceptor page's existing section of the same name, including the transitionMode note the review asked for (an alias target is always non-nested, so the same exemption applies). Adds an entry for the Alias Interceptor to the @StrutsParameter page's Where authorization applies list. Version cited as 7.5.0 to match the pattern of the two prior related fixes (WW-5752, WW-5753) and the current pom.xml SNAPSHOT; WW-5758's Fix Version is not yet set, so this may need updating once it is. * Fix factual error, contradictory wording, and review-link typo Addresses lukaszlenart's review on struts-site#347: - An alias target CAN be a nested path (an alias map value like 'bean.bar' is valid - nothing in AliasInterceptor restricts it, confirmed against the merged code). The page previously claimed 'always non-nested', which is wrong; transitionMode only exempts the non-nested case, so a nested target still needs the annotation regardless of transitionMode. - Fixed 'always requires... governed by struts.parameters.requireAnnotations' - contradictory wording, since with the setting off nothing is required. - Dropped 'separately-authorized' describing a stack-resolved source value - it can be any state an earlier action holds, not necessarily something bound through an annotated setter. - Added an 'Upgrading an existing application' note: an app using the documented foo/bar example needs bar annotated once struts.parameters.requireAnnotations is enabled, or the alias is silently skipped. * Fix ModelDriven exemption claim and opt-in wording Addresses lukaszlenart's second review on struts-site#347: 1. The ModelDriven exemption sentence claimed it applies unconditionally, which isn't true in defaultStack (alias runs before modelDriven there, so the model isn't on the stack yet - the check and the write both land on the action). Reworded to his exact suggested text: the exemption only applies in a custom stack that places alias after modelDriven. 2. 'once struts.parameters.requireAnnotations is enabled' read as opt-in; it's on by default since 7.0.0. 'silently skipped (logged at WARN)' was self-contradictory. Reworded to his suggested text.
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.