This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch WW-5659-lazy-params-request-scoping in repository https://gitbox.apache.org/repos/asf/struts.git
commit 9d66a7b9f44953c2adadee4d8f892fa486ecb507 Author: Lukasz Lenart <[email protected]> AuthorDate: Mon Jul 27 11:31:30 2026 +0200 WW-5659 fix(core): reject uploads when the policy cannot be resolved --- .../interceptor/AbstractFileUploadInterceptor.java | 12 +++++++ .../apache/struts2/interceptor/UploadPolicy.java | 21 ++++++++++++ .../org/apache/struts2/struts-messages.properties | 4 +++ .../ActionFileUploadInterceptorTest.java | 38 ++++++++++++++++++++++ 4 files changed, 75 insertions(+) diff --git a/core/src/main/java/org/apache/struts2/interceptor/AbstractFileUploadInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/AbstractFileUploadInterceptor.java index f8529732b..dac450b45 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/AbstractFileUploadInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/AbstractFileUploadInterceptor.java @@ -56,6 +56,7 @@ public abstract class AbstractFileUploadInterceptor extends AbstractInterceptor public static final String STRUTS_MESSAGES_INVALID_CONTENT_TYPE_KEY = "struts.messages.invalid.content.type"; public static final String STRUTS_MESSAGES_ERROR_CONTENT_TYPE_NOT_ALLOWED_KEY = "struts.messages.error.content.type.not.allowed"; public static final String STRUTS_MESSAGES_ERROR_FILE_EXTENSION_NOT_ALLOWED_KEY = "struts.messages.error.file.extension.not.allowed"; + public static final String STRUTS_MESSAGES_ERROR_UPLOAD_POLICY_UNRESOLVED_KEY = "struts.messages.error.upload.policy.unresolved"; private final UploadPolicy configuredPolicy = new UploadPolicy(); @@ -140,6 +141,17 @@ public abstract class AbstractFileUploadInterceptor extends AbstractInterceptor return false; } + if (policy.isUnresolved()) { + String errMsg = getTextMessage(action, STRUTS_MESSAGES_ERROR_UPLOAD_POLICY_UNRESOLVED_KEY, new String[]{ + inputName, originalFilename, String.join(", ", policy.getUnresolvedParams()) + }); + if (validation != null) { + validation.addFieldError(inputName, errMsg); + } + LOG.warn(errMsg); + return false; + } + if (policy.getMaximumSize() != null && policy.getMaximumSize() < file.length()) { String errMsg = getTextMessage(action, STRUTS_MESSAGES_ERROR_FILE_TOO_LARGE_KEY, new String[]{ inputName, originalFilename, file.getName(), "" + file.length(), getMaximumSizeStr(action, policy.getMaximumSize()) diff --git a/core/src/main/java/org/apache/struts2/interceptor/UploadPolicy.java b/core/src/main/java/org/apache/struts2/interceptor/UploadPolicy.java index a9e444157..08702dee4 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/UploadPolicy.java +++ b/core/src/main/java/org/apache/struts2/interceptor/UploadPolicy.java @@ -21,6 +21,7 @@ package org.apache.struts2.interceptor; import org.apache.struts2.util.TextParseUtil; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Set; /** @@ -36,6 +37,7 @@ public class UploadPolicy extends DisableParams { private Long maximumSize; private Set<String> allowedTypes = Collections.emptySet(); private Set<String> allowedExtensions = Collections.emptySet(); + private final Set<String> unresolvedParams = new LinkedHashSet<>(); public UploadPolicy() { } @@ -45,6 +47,7 @@ public class UploadPolicy extends DisableParams { this.maximumSize = other.maximumSize; this.allowedTypes = other.allowedTypes; this.allowedExtensions = other.allowedExtensions; + this.unresolvedParams.addAll(other.unresolvedParams); } /** @@ -80,6 +83,24 @@ public class UploadPolicy extends DisableParams { return allowedExtensions; } + /** + * A parameter that could not be resolved makes this policy unusable: the upload is rejected + * rather than validated against a partially-resolved policy, so a broken expression cannot + * silently relax validation. + */ + @Override + public void unresolved(String paramName) { + unresolvedParams.add(paramName); + } + + public boolean isUnresolved() { + return !unresolvedParams.isEmpty(); + } + + public Set<String> getUnresolvedParams() { + return Collections.unmodifiableSet(unresolvedParams); + } + /** * @return an independent copy, used to seed a per-invocation policy from the configured one */ diff --git a/core/src/main/resources/org/apache/struts2/struts-messages.properties b/core/src/main/resources/org/apache/struts2/struts-messages.properties index 63514b000..ecd99000b 100644 --- a/core/src/main/resources/org/apache/struts2/struts-messages.properties +++ b/core/src/main/resources/org/apache/struts2/struts-messages.properties @@ -50,6 +50,10 @@ struts.messages.error.content.type.not.allowed=Content-Type not allowed: {0} "{1 # 2 - file name after uploading the file # 3 - content type of the file struts.messages.error.file.extension.not.allowed=File extension not allowed: {0} "{1}" "{2}" {3} +# 0 - input name +# 1 - original filename +# 2 - comma-delimited list of unresolved parameter names +struts.messages.error.upload.policy.unresolved=The upload validation policy could not be resolved, rejecting the file: {0} "{1}"; unresolved parameters: {2} # dedicated messages used to handle various problems with file upload - check {@link JakartaMultiPartRequest#parse(HttpServletRequest, String)} # params depend on exception being handled # FileUploadByteCountLimitException diff --git a/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java index e9bca76cc..ab31c3d18 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java @@ -1160,4 +1160,42 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase { } } + public void testUnresolvedPolicyRejectsTheUpload() throws Exception { + ActionFileUploadInterceptor interceptor = new ActionFileUploadInterceptor(); + container.inject(interceptor); + + MyDynamicFileUploadAction action = new MyDynamicFileUploadAction(); + action.setAllowedMimeTypes(null); // ${allowedMimeTypes} will not resolve + container.inject(action); + + runUploadAttempt(interceptor, action, createUploadRequest("f.txt", "text/plain", plainContent)); + + assertThat(action.getUploadFiles()).isNull(); + assertThat(action.getFieldErrors()).containsKey("file"); + } + + public void testResolvedPolicyStillAcceptsTheUpload() throws Exception { + ActionFileUploadInterceptor interceptor = new ActionFileUploadInterceptor(); + container.inject(interceptor); + + MyDynamicFileUploadAction action = new MyDynamicFileUploadAction(); + action.setAllowedMimeTypes("text/plain"); + container.inject(action); + + runUploadAttempt(interceptor, action, createUploadRequest("f.txt", "text/plain", plainContent)); + + assertThat(action.hasFieldErrors()).isFalse(); + assertThat(action.getUploadFiles()).isNotNull().hasSize(1); + } + + public void testUploadPolicyTracksUnresolvedParams() { + UploadPolicy policy = new UploadPolicy(); + assertThat(policy.isUnresolved()).isFalse(); + + policy.unresolved("allowedTypes"); + + assertThat(policy.isUnresolved()).isTrue(); + assertThat(policy.getUnresolvedParams()).containsExactly("allowedTypes"); + } + }
