This is an automated email from the ASF dual-hosted git repository.

reiern70 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/wicket.git


The following commit(s) were added to refs/heads/master by this push:
     new 483b83ca62 Sign file upload resource constraints
483b83ca62 is described below

commit 483b83ca62a606ae4a9fd3ad01c93691105acf5c
Author: mundur <[email protected]>
AuthorDate: Tue Jul 14 22:46:09 2026 +0800

    Sign file upload resource constraints
---
 ...loadToResourceFieldSecurityTest$UploadPage.html |   5 +
 .../FileUploadToResourceFieldSecurityTest.java     | 188 +++++++++++++++++++++
 .../resource/AbstractFileUploadResource.java       |  71 +++++++-
 .../upload/resource/FileUploadToResourceField.java |   2 +
 .../upload/resource/FileUploadToResourceField.js   |   9 +-
 5 files changed, 268 insertions(+), 7 deletions(-)

diff --git 
a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceFieldSecurityTest$UploadPage.html
 
b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceFieldSecurityTest$UploadPage.html
new file mode 100644
index 0000000000..2658b563d3
--- /dev/null
+++ 
b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceFieldSecurityTest$UploadPage.html
@@ -0,0 +1,5 @@
+<html xmlns:wicket="http://wicket.apache.org";>
+<body>
+       <input type="file" wicket:id="upload"/>
+</body>
+</html>
diff --git 
a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceFieldSecurityTest.java
 
b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceFieldSecurityTest.java
new file mode 100644
index 0000000000..0251ba071a
--- /dev/null
+++ 
b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceFieldSecurityTest.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.markup.html.form.upload.resource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.List;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.util.lang.Bytes;
+import org.apache.wicket.util.file.File;
+import org.apache.wicket.util.tester.WicketTestCase;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+class FileUploadToResourceFieldSecurityTest extends WicketTestCase
+{
+       private Path tempDir;
+
+       @AfterEach
+       void tearDown() throws IOException
+       {
+               if (tempDir != null)
+               {
+                       try (var paths = Files.walk(tempDir))
+                       {
+                               
paths.sorted(Comparator.reverseOrder()).forEach(path ->
+                               {
+                                       try
+                                       {
+                                               Files.deleteIfExists(path);
+                                       }
+                                       catch (IOException e)
+                                       {
+                                               throw new RuntimeException(e);
+                                       }
+                               });
+                       }
+               }
+       }
+
+       @Test
+       void acceptsUploadWithRenderedLimits() throws Exception
+       {
+               FolderUploadsFileManager fileManager = newFileManager();
+               UploadPage page = new UploadPage(new 
TestFileUploadResourceReference(fileManager));
+               tester.startPage(page);
+
+               File upload = createUploadFile("allowed.txt", 256);
+               submitUpload(page.field.buildUploadUrl(), upload);
+
+               File storedFile = fileManager.getFile(page.field.getMarkupId(), 
upload.getName());
+               assertTrue(storedFile.exists());
+               assertEquals(upload.length(), storedFile.length());
+       }
+
+       @Test
+       void rejectsTamperedUploadLimits() throws Exception
+       {
+               FolderUploadsFileManager fileManager = newFileManager();
+               UploadPage page = new UploadPage(new 
TestFileUploadResourceReference(fileManager));
+               tester.startPage(page);
+
+               File upload = createUploadFile("oversized.txt", 4096);
+               String validToken = 
AbstractFileUploadResource.createUploadToken(page.field.getMarkupId(),
+                       page.field.getMaxSize(), page.field.getFileMaxSize(), 
page.field.getFileCountMax());
+               String tamperedUrl = page.field.getResourceUrl() + '?' +
+                       AbstractFileUploadResource.UPLOAD_ID + '=' + 
page.field.getMarkupId() + '&' +
+                       AbstractFileUploadResource.MAX_SIZE + '=' + 
Bytes.kilobytes(64).bytes() + '&' +
+                       AbstractFileUploadResource.FILE_COUNT_MAX + '=' + 
page.field.getFileCountMax() + '&' +
+                       AbstractFileUploadResource.UPLOAD_TOKEN + '=' + 
validToken;
+
+               submitUpload(tamperedUrl, upload);
+
+               File storedFile = fileManager.getFile(page.field.getMarkupId(), 
upload.getName());
+               assertFalse(storedFile.exists());
+               
assertTrue(tester.getLastResponseAsString().contains("uploadFailed"));
+       }
+
+       private FolderUploadsFileManager newFileManager() throws IOException
+       {
+               tempDir = Files.createTempDirectory("wicket-upload-security");
+               return new FolderUploadsFileManager(new File(tempDir.toFile()));
+       }
+
+       private File createUploadFile(String fileName, int size) throws 
IOException
+       {
+               Path filePath = tempDir.resolve(fileName);
+               Files.write(filePath, new byte[size]);
+               return new File(filePath.toFile());
+       }
+
+       private void submitUpload(String url, File upload)
+       {
+               tester.getRequest().setMethod("POST");
+               
tester.getRequest().addFile(AbstractFileUploadResource.PARAM_NAME, upload, 
"text/plain");
+               tester.executeUrl(url);
+       }
+
+       static class UploadPage extends WebPage
+       {
+               private static final long serialVersionUID = 1L;
+
+               final TestField field;
+
+               UploadPage(TestFileUploadResourceReference resourceReference)
+               {
+                       field = new TestField("upload", resourceReference);
+                       field.setMaxSize(Bytes.kilobytes(1));
+                       add(field);
+               }
+       }
+
+       static class TestField extends FileUploadToResourceField
+       {
+               private static final long serialVersionUID = 1L;
+
+               private final TestFileUploadResourceReference resourceReference;
+
+               TestField(String id, TestFileUploadResourceReference 
resourceReference)
+               {
+                       super(id);
+                       this.resourceReference = resourceReference;
+               }
+
+               @Override
+               protected FileUploadResourceReference 
getFileUploadResourceReference()
+               {
+                       return resourceReference;
+               }
+
+               @Override
+               protected void onUploadSuccess(AjaxRequestTarget target, 
List<UploadInfo> fileInfos)
+               {
+               }
+
+               String buildUploadUrl()
+               {
+                       StringBuilder url = new StringBuilder(getResourceUrl());
+                       
url.append('?').append(AbstractFileUploadResource.UPLOAD_ID).append('=')
+                               .append(getMarkupId());
+                       
url.append('&').append(AbstractFileUploadResource.MAX_SIZE).append('=')
+                               .append(getMaxSize().bytes());
+                       
url.append('&').append(AbstractFileUploadResource.FILE_COUNT_MAX).append('=')
+                               .append(getFileCountMax());
+                       
url.append('&').append(AbstractFileUploadResource.UPLOAD_TOKEN).append('=')
+                               
.append(AbstractFileUploadResource.createUploadToken(getMarkupId(), 
getMaxSize(),
+                                       getFileMaxSize(), getFileCountMax()));
+                       return url.toString();
+               }
+
+               String getResourceUrl()
+               {
+                       return urlFor(resourceReference, new 
PageParameters()).toString();
+               }
+       }
+
+       static class TestFileUploadResourceReference extends 
FileUploadResourceReference
+       {
+               private static final long serialVersionUID = 1L;
+
+               TestFileUploadResourceReference(IUploadsFileManager 
uploadFileManager)
+               {
+                       super(uploadFileManager);
+               }
+       }
+}
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/AbstractFileUploadResource.java
 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/AbstractFileUploadResource.java
index 50cf91ff85..dea29a3494 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/AbstractFileUploadResource.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/AbstractFileUploadResource.java
@@ -17,6 +17,8 @@
 package org.apache.wicket.markup.html.form.upload.resource;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -60,6 +62,10 @@ public abstract class AbstractFileUploadResource extends 
AbstractResource
        private static final Logger LOG = 
LoggerFactory.getLogger(AbstractFileUploadResource.class);
 
        public static final String PARAM_NAME = "WICKET-FILE-UPLOAD";
+       public static final String MAX_SIZE = "maxSize";
+       public static final String FILE_MAX_SIZE = "fileMaxSize";
+       public static final String FILE_COUNT_MAX = "fileCountMax";
+       public static final String UPLOAD_TOKEN = "uploadToken";
 
        /**
         * This resource is usually an application singleton. Thus, client side 
pass
@@ -81,6 +87,14 @@ public abstract class AbstractFileUploadResource extends 
AbstractResource
                this.fileManager = fileManager;
        }
 
+       static String createUploadToken(String uploadId, Bytes maxSize, Bytes 
fileMaxSize,
+               long fileCountMax)
+       {
+               String plainText = serializeUploadSettings(uploadId, maxSize, 
fileMaxSize, fileCountMax);
+               return 
Application.get().getSecuritySettings().getCryptFactory().newCrypt()
+                       .encryptUrlSafe(plainText);
+       }
+
        /**
         * Reads and stores the uploaded files
         *
@@ -101,6 +115,12 @@ public abstract class AbstractFileUploadResource extends 
AbstractResource
                Bytes maxSize = getMaxSize(webRequest);
                Bytes fileMaxSize = getFileMaxSize(webRequest);
                long fileCountMax = getFileCountMax(webRequest);
+               if (!hasValidUploadToken(webRequest, uploadId, maxSize, 
fileMaxSize, fileCountMax))
+               {
+                       LOG.warn("Rejected upload for '{}' because the signed 
upload settings were invalid",
+                               uploadId);
+                       return createErrorResponse(resourceResponse, 
Form.UPLOAD_FAILED_RESOURCE_KEY);
+               }
                try
                {
                        MultipartServletWebRequest multiPartRequest = 
webRequest.newMultipartWebRequest(maxSize, uploadId);
@@ -186,6 +206,51 @@ public abstract class AbstractFileUploadResource extends 
AbstractResource
                return resourceResponse;
        }
 
+       private static String serializeUploadSettings(String uploadId, Bytes 
maxSize, Bytes fileMaxSize,
+               long fileCountMax)
+       {
+               return uploadId + '\n' + bytesValue(maxSize) + '\n' + 
bytesValue(fileMaxSize) + '\n' +
+                       fileCountMax;
+       }
+
+       private static String bytesValue(Bytes value)
+       {
+               return value != null ? Long.toString(value.bytes()) : "";
+       }
+
+       private boolean hasValidUploadToken(ServletWebRequest webRequest, 
String uploadId, Bytes maxSize,
+               Bytes fileMaxSize, long fileCountMax)
+       {
+               String actualToken = getParameterValue(webRequest, 
UPLOAD_TOKEN).toOptionalString();
+               if (actualToken == null)
+               {
+                       return false;
+               }
+
+               String expectedToken = createUploadToken(uploadId, maxSize, 
fileMaxSize, fileCountMax);
+               return 
MessageDigest.isEqual(expectedToken.getBytes(StandardCharsets.UTF_8),
+                       actualToken.getBytes(StandardCharsets.UTF_8));
+       }
+
+       private ResourceResponse createErrorResponse(ResourceResponse 
resourceResponse, String errorMessage)
+       {
+               resourceResponse.setContentType("application/json");
+               resourceResponse.setStatusCode(HttpServletResponse.SC_OK);
+               JSONObject json = new JSONObject();
+               json.put("error", true);
+               json.put("errorMessage", errorMessage);
+               String error = json.toString();
+               resourceResponse.setWriteCallback(new WriteCallback()
+               {
+                       @Override
+                       public void writeData(Attributes attributes) throws 
IOException
+                       {
+                               attributes.getResponse().write(error);
+                       }
+               });
+               return resourceResponse;
+       }
+
 
        protected String getFileUploadExceptionKey(final FileUploadException e)
        {
@@ -263,7 +328,7 @@ public abstract class AbstractFileUploadResource extends 
AbstractResource
        {
                try
                {
-                       return Bytes.bytes(getParameterValue(webRequest, 
"maxSize").toLong());
+                       return Bytes.bytes(getParameterValue(webRequest, 
MAX_SIZE).toLong());
                }
                catch (StringValueConversionException e)
                {
@@ -279,7 +344,7 @@ public abstract class AbstractFileUploadResource extends 
AbstractResource
         */
        private Bytes getFileMaxSize(ServletWebRequest webRequest)
        {
-               long fileMaxSize = getParameterValue(webRequest, 
"fileMaxSize").toLong(-1);
+               long fileMaxSize = getParameterValue(webRequest, 
FILE_MAX_SIZE).toLong(-1);
                return fileMaxSize > 0 ? Bytes.bytes(fileMaxSize) : null;
        }
 
@@ -300,7 +365,7 @@ public abstract class AbstractFileUploadResource extends 
AbstractResource
         */
        private long getFileCountMax(ServletWebRequest webRequest)
        {
-               return getParameterValue(webRequest, "fileCountMax").toLong(-1);
+               return getParameterValue(webRequest, FILE_COUNT_MAX).toLong(-1);
        }
 
        /**
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.java
 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.java
index fbff4d91e0..47d10fae0e 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.java
@@ -373,6 +373,8 @@ public abstract class FileUploadToResourceField extends 
FileUploadField
             jsonObject.put("fileMaxSize", fileMaxSize.bytes());
         }
         jsonObject.put("fileCountMax", getFileCountMax());
+        jsonObject.put("uploadToken", 
AbstractFileUploadResource.createUploadToken(getMarkupId(),
+                getMaxSize(), fileMaxSize, getFileCountMax()));
         response.render(OnDomReadyHeaderItem.forScript("Wicket.Timer."
                 + getMarkupId() + " = new Wicket.FileUploadToResourceField("
                 + jsonObject + ","
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.js
 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.js
index 815e66912f..1ed5e0808b 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.js
+++ 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.js
@@ -28,13 +28,14 @@
         this.settings = settings;
         this.inputName = settings.inputName;
         this.input = document.getElementById(this.inputName);
-        this.resourceUrl = settings.resourceUrl + "?uploadId=" + 
this.inputName + "&maxSize=" + this.settings.maxSize;
+        this.resourceUrl = settings.resourceUrl + "?uploadId=" + 
encodeURIComponent(this.inputName) + "&maxSize=" + 
encodeURIComponent(this.settings.maxSize);
         if (this.settings.fileMaxSize != null) {
-            this.resourceUrl = this.resourceUrl + "&fileMaxSize=" + 
this.settings.fileMaxSize;
+            this.resourceUrl = this.resourceUrl + "&fileMaxSize=" + 
encodeURIComponent(this.settings.fileMaxSize);
         }
         if (this.settings.fileCountMax != null) {
-            this.resourceUrl = this.resourceUrl + "&fileCountMax=" + 
this.settings.fileCountMax;
+            this.resourceUrl = this.resourceUrl + "&fileCountMax=" + 
encodeURIComponent(this.settings.fileCountMax);
         }
+        this.resourceUrl = this.resourceUrl + "&uploadToken=" + 
encodeURIComponent(this.settings.uploadToken);
         this.ajaxCallBackUrl = settings.ajaxCallBackUrl;
         this.clientBeforeSendCallBack = clientBeforeSendCallBack;
         this.clientSideSuccessCallBack = clientSideSuccessCallBack;
@@ -106,4 +107,4 @@
             Wicket.Log.log("Too late to cancel upload for field '"  + 
this.inputName +  "': the upload has already finished.");
         }
     }
-})();
\ No newline at end of file
+})();

Reply via email to