WW-5585 Keep dynamic upload validation request-scoped - #1815
Conversation
Follow-up to the dynamic upload-policy support added in WW-5585. When lazy upload parameters are resolved per request, keep the effective policy on request-local state instead of writing it back to the shared interceptor instance. Add a regression that covers the concurrent overwrite case.
There was a problem hiding this comment.
Pull request overview
This PR addresses a concurrency issue in Struts’ file upload validation when allowedTypes, allowedExtensions, or maximumSize are lazily resolved via ${...}: previously, the resolved values could be written back onto a shared interceptor instance and bleed across concurrent requests. The change keeps dynamically-resolved upload validation policy request-scoped for the lifetime of the interceptor invocation and clears it afterward, while preserving existing behavior for static interceptor configuration.
Changes:
- Add a request-scoped
UploadValidationPolicy(viaThreadLocal) inAbstractFileUploadInterceptorand use it duringacceptFile(...). - Track when lazy param injection is in progress (
WithLazyParams.LazyParamInjector), so setters can route dynamically injected values into the request-scoped policy rather than the interceptor’s shared fields. - Ensure request-scoped policy is cleared in
ActionFileUploadInterceptor.intercept()and add a concurrent regression test.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java | Refactors dynamic-policy tests to inject ${...} params and adds a concurrency regression test to ensure per-request isolation. |
| core/src/main/java/org/apache/struts2/interceptor/WithLazyParams.java | Adds a thread-local “injection in progress” marker around lazy param injection. |
| core/src/main/java/org/apache/struts2/interceptor/ActionFileUploadInterceptor.java | Clears request-scoped upload validation policy in a finally block after interception. |
| core/src/main/java/org/apache/struts2/interceptor/AbstractFileUploadInterceptor.java | Implements request-scoped upload validation policy storage and uses it during file acceptance checks. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private String runUploadAttempt(ActionFileUploadInterceptor actionFileUploadInterceptor, | ||
| MyDynamicFileUploadAction action, | ||
| MockHttpServletRequest uploadRequest, | ||
| String allowedTypes) throws Exception { | ||
| MultiPartRequestWrapper multiPartRequest = createMultipartRequest(uploadRequest, -1, -1, 3, -1); | ||
| ValueStack valueStack = container.getInstance(ValueStackFactory.class).createValueStack(); | ||
| valueStack.push(action); | ||
|
|
|
Thanks for this — the underlying problem is real and you diagnosed it correctly. I traced it independently and it holds up:
I don't want to merge this particular fix though, for a few reasons:
So rather than patch the one implementer, I'd like to fix the public interface WithLazyParams<P> {
P newLazyParams();
String intercept(ActionInvocation invocation, P lazyParams) throws Exception;
}That keeps OGNL type conversion (the holder has typed setters, so I've filed WW-5659 to track it and I'm writing up the design now. Your regression test is the valuable part of this PR — the scenario it pins down is exactly what the new design has to keep passing, and I'd like to carry it over (with attribution) once the interface lands. If you'd like to take the implementation on the new contract, say so and it's yours; otherwise I'll pick it up and credit you on the ticket. One process note for next time, not a criticism of this one: when a change touches something that could weaken a security control — upload validation, parameter filtering, OGNL access — please email [email protected] before opening a public PR. |
|
Thanks for the detailed review. I’d like to take WW-5659 on this branch direction. I’ll rework this around the explicit per-invocation lazy-params contract and keep the concurrency regression scenario in place. |
|
Pushed a rework on top of this branch for WW-5659. The lazy params now live in a per-invocation holder instead of mutating the singleton interceptor instance. |
…ions
A ${...} param is resolved per invocation into the params holder, so applying
its raw text to the interceptor at configuration time only seeded an
unevaluated literal - allowedTypes held "${uploadConfig.allowedMimeTypes}",
matching no content type - or failed conversion outright for a typed property
such as the Long maximumSize.
Withhold those params at build time; static params still apply and still seed
the holder, which is what a lazy param falling back has to fall back to. Also
makes InterceptorParams.unresolved's javadoc about the retained value honest.
Idea from @deprrous in GitHub PR #1815.
Co-Authored-By: deprrous <[email protected]>
|
Thanks for the rework — I read it properly and it's a real step up. You independently found several things that took my side multiple review rounds to catch:
Where this is goingI'm going to land #1816 rather than this PR, and I want to be straight with you about why, because your design had one genuine advantage over mine. Your What settles it is release planning rather than design: 7.3.0 already carries other compatibility-breaking changes, so there is no 7.2.2 to target. Once the fix ships in a version that breaks compatibility anyway, the compat-preserving shape stops buying anything, and the tradeoff inverts — a single mandatory contract is easier to reason about than a mandatory one plus an optional one, and it removes the unsafe path instead of letting implementers opt out of it. In your version the legacy Alongside that, #1816 carries work this PR doesn't: fail-closed handling when an expression can't be resolved (previously an unresolvable One bug worth knowing about, wherever it landsIn this PR a lazily resolved What I've taken from your PR
Your concurrency regression test from the first round is also in #1816, carried over with attribution — the scenario it pins is the acceptance criterion for the whole change, and reverting the fix still makes it fail exactly as the bug describes. So: I'm closing this one, but two pieces of it ship, and you're credited on the ticket and in the commits. The allowlist catch in particular was independent work on a trap that had already bitten me — thank you for it. One process note, same as last time and still worth repeating: when a change touches upload validation, parameter filtering, or OGNL access, please email [email protected] before opening a public PR. |
Follow-up to WW-5585.
When
allowedTypes,allowedExtensions, ormaximumSizeare resolved lazily from${...}, those resolved values currently get written back onto the shared interceptor instance. That lets concurrent requests bleed into each other before file validation runs.This keeps the lazily resolved upload policy request-scoped for the lifetime of the interceptor call and clears it afterwards. Static interceptor configuration still behaves the same.
Tests:
./mvnw -pl core -Dtest=org.apache.struts2.interceptor.ActionFileUploadInterceptorTest#testConcurrentDynamicPoliciesStayIsolatedPerRequest test./mvnw -pl core -Dtest=org.apache.struts2.interceptor.ActionFileUploadInterceptorTest,org.apache.struts2.DefaultActionInvocationTest test