Skip to content

WW-5829 fix(interceptor): apply parameter value filters to every value of a multi-valued parameter - #2087

Open
g0w6y wants to merge 2 commits into
apache:mainfrom
g0w6y:WW-5829-multivalue-parameter-value-filters
Open

g0w6y wants to merge 2 commits into
apache:mainfrom
g0w6y:WW-5829-multivalue-parameter-value-filters

Conversation

@g0w6y

@g0w6y g0w6y commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Fixes WW-5829

What & why

ParametersInterceptor hands only Parameter.getValue() (the first value) to ParameterValueAware.acceptableParameterValue and to the excluded/accepted value patterns, but then sets the whole String[] on the action. So ?tags=ok&tags=evil passes a filter that rejects evil, while ?tags=evil and ?tags=evil&tags=ok are rejected.

JSONInterceptor already checks every element of a JSON array (WW-4858). This checks every value of a repeated form parameter too, with one difference: JSONInterceptor drops only the bad list element, while here one rejected value rejects the whole parameter.

Change

isAcceptableParameterValue and isAcceptableParameterValueAware now check every value of a multi-valued parameter (small private helper valuesToCheck). One rejected value rejects the whole parameter, which is what already happens when the first value is rejected. Single-valued and empty parameters go through the same calls with the same argument as before.

How verified

Three tests added to ParametersInterceptorTest (excluded patterns, accepted patterns, ParameterValueAware), each covering single value, all values good, first value bad, second value bad.

  • Without the change: Tests run: 48, Failures: 3 (all three new tests: secondBad reached the stack).
  • With the change: ParametersInterceptorTest (48), ParameterAuthorizerTest (42) and StrutsParameterAnnotationTest (33) pass.
  • Also checked against a running Struts Boot app with an action implementing ParameterValueAware that rejects evil: ?tags=ok&tags=evil was accepted before (the action received [ok, evil]) and is rejected with the change; ?tags=ok&tags=fine still binds.

Not changed

  • No new configuration; value filters stay opt-in.
  • No partial filtering: a bad value does not just drop that one value (JSONInterceptor drops the bad list element; here the parameter is rejected whole, as it already is for a bad first value).
  • Parameter name checks are untouched.

Risk

Behaviour changes only for actions that configure value patterns or implement ParameterValueAware and receive repeated parameters where a later value is rejected; those requests were previously (wrongly) accepted. Cost is one extra pattern match per additional value.

…e of a multi-valued parameter

ParametersInterceptor passed only Parameter.getValue() (the first value) to
ParameterValueAware and the excluded/accepted value patterns, so a request such
as a=ok&a=evil was accepted although "evil" would have been rejected. Check each
value. Unlike JSONInterceptor, which drops only the bad list element, a single
rejected value rejects the whole parameter (as a rejected first value already
did). Single-valued and empty parameters behave as before.

@lukaszlenart lukaszlenart left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the change looks right to me. Every value that ends up bound is now checked, ActionMappingParametersInterceptor picks it up without changes, and single-valued and empty parameters behave as before. I ran ParametersInterceptorTest, ParameterAuthorizerTest, StrutsParameterAnnotationTest and ActionMappingParametersInterceptorTest, and all pass. With the ParametersInterceptor change reverted, exactly the three new tests fail.

Two test gaps to close before merging, plus three optional cleanups, as inline comments.

Map<String, Object> parameters = new HashMap<>();
parameters.put("single", new String[]{"fooValue"});
parameters.put("allGood", new String[]{"fooValue", "fooValue2"});
parameters.put("secondBad", new String[]{"fooValue", "barValue"});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says each test covers single / all good / first bad / second bad, but this one has no firstBad case. Could you add e.g. parameters.put("firstBad", new String[]{"barValue", "fooValue"});, so a version that skips index 0 or checks only the last value also fails here?

parameters.put("secondBad", new String[]{"fooValue", "${2+2}"});
pi.applyParameters(new NoParametersAction(), stack, HttpParameters.create(parameters).build());

assertEquals(Set.of("single", "allGood"), actual.keySet());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three tests assert only keySet(). A later change that accepts allGood but binds only its first value would still pass them. Could you also assert the bound value, e.g. assertArrayEquals(new String[]{"fooValue", "barValue"}, (String[]) actual.get("allGood"));, in each of the three tests?

protected boolean isAcceptableParameterValue(Parameter param, Object action) {
return isAcceptableParameterValueAware(param, action) && isAcceptableValue(param.getName(), param.getValue());
return isAcceptableParameterValueAware(param, action)
&& Arrays.stream(valuesToCheck(param)).allMatch(value -> isAcceptableValue(param.getName(), value));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: valuesToCheck runs twice per parameter, and each run rebuilds the array several times through isMultiple() (via isDefined()) and getMultipleValues(). That's about six toStringArray() calls per multi-valued parameter. Computing the array once here and passing it to both checks would avoid that.

Also optional: with no value patterns configured (the default), each value still goes through isParamValueExcluded/isParamValueAccepted, and each logs a "not defined" DEBUG line. A large checkbox list now produces two lines per value. An early return when neither pattern set is configured would keep that to the old one pass.

|| Arrays.stream(valuesToCheck(param)).allMatch(valueAware::acceptableParameterValue);
}

private String[] valuesToCheck(Parameter param) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: since isAcceptableParameterValue / isAcceptableParameterValueAware are protected extension points, making this helper protected too would let a subclass that overrides one of them reuse the same expansion instead of falling back to param.getValue().

…aluesToCheck

Close the two required test gaps from review:

- testAcceptedValuePatternsApplyToEveryValueOfMultiValuedParameter had no
  firstBad case, unlike the other two tests, so a version that skipped
  index 0 wouldn't have been caught here.
- All three multi-value tests asserted only actual.keySet(), so a version
  that accepted a parameter but bound only its first value would still
  have passed. Add assertArrayEquals on the bound value in each.

Also widen valuesToCheck from private to protected, so a subclass
overriding isAcceptableParameterValue/isAcceptableParameterValueAware can
reuse the same expansion instead of falling back to param.getValue().
@g0w6y

g0w6y commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review. Closed both required gaps (16b0e2a):

  • Added the missing firstBad case to testAcceptedValuePatternsApplyToEveryValueOfMultiValuedParameter.
  • All three multi-value tests now also assertArrayEquals on the bound value, not just actual.keySet().

Also took the valuesToCheck visibility nit (now protected).

Left the double-computation optimization as is: avoiding it cleanly means either bypassing virtual dispatch on isAcceptableParameterValueAware (a pre-existing protected extension point) when called from isAcceptableParameterValue, or changing that method's signature -- both are real behavior/compatibility risk for a non-blocking perf nitpick, so happy to take a specific suggested shape if you have one in mind, but didn't want to guess.

Full core suite: 3628/0 failures. ParametersInterceptorTest, ParameterAuthorizerTest, StrutsParameterAnnotationTest, ActionMappingParametersInterceptorTest all green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants