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

dashorst pushed a commit to branch wicket-9.x
in repository https://gitbox.apache.org/repos/asf/wicket.git

commit 713f77aa9ccfefac359e0fee4020fa3f6ff4eaed
Author: reiern70 <[email protected]>
AuthorDate: Wed Sep 30 13:06:22 2026 -0500

    Sign file upload resource constraints
    
    FileUploadToResourceField now renders an encrypted token that covers the
    upload id and the maxSize, fileMaxSize and fileCountMax it was configured
    with, and AbstractFileUploadResource only accepts an upload whose token
    matches the values in the request.
    
    Client code that posts to the upload resource without the token rendered by
    the field now receives the uploadFailed error.
---
 .../resource/AbstractFileUploadResource.java       |  84 +++++++-
 .../upload/resource/FileUploadToResourceField.java |   2 +
 .../upload/resource/FileUploadToResourceField.js   |   9 +-
 ...loadToResourceFieldSecurityTest$UploadPage.html |  21 ++
 .../FileUploadToResourceFieldSecurityTest.java     | 217 +++++++++++++++++++++
 5 files changed, 326 insertions(+), 7 deletions(-)

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 8e4dee88a6..10d907064e 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;
@@ -57,6 +59,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
@@ -78,6 +84,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
         *
@@ -98,6 +112,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);
@@ -183,6 +203,64 @@ 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 = 
webRequest.getRequestParameters().getParameterValue(UPLOAD_TOKEN)
+                       .toOptionalString();
+               if (actualToken == null)
+               {
+                       return false;
+               }
+
+               // the token is authenticated but not deterministic - 
encryption uses a fresh nonce per
+               // message - so it cannot be validated by re-encrypting and 
comparing ciphertext. Decrypt
+               // it instead and compare the settings it carries; the 
authentication tag is what makes it
+               // unforgeable, and decryption fails on anything that was 
tampered with.
+               String decryptedToken = 
Application.get().getSecuritySettings().getCryptFactory().newCrypt()
+                       .decryptUrlSafe(actualToken);
+               if (decryptedToken == null)
+               {
+                       return false;
+               }
+
+               String expectedSettings = serializeUploadSettings(uploadId, 
maxSize, fileMaxSize,
+                       fileCountMax);
+               return 
MessageDigest.isEqual(expectedSettings.getBytes(StandardCharsets.UTF_8),
+                       decryptedToken.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)
        {
@@ -252,7 +330,7 @@ public abstract class AbstractFileUploadResource extends 
AbstractResource
        {
                try
                {
-                       return 
Bytes.bytes(webRequest.getRequestParameters().getParameterValue("maxSize").toLong());
+                       return 
Bytes.bytes(webRequest.getRequestParameters().getParameterValue(MAX_SIZE).toLong());
                }
                catch (StringValueConversionException e)
                {
@@ -268,7 +346,7 @@ public abstract class AbstractFileUploadResource extends 
AbstractResource
         */
        private Bytes getFileMaxSize(ServletWebRequest webRequest)
        {
-               long fileMaxSize = 
webRequest.getRequestParameters().getParameterValue("fileMaxSize").toLong(-1);
+               long fileMaxSize = 
webRequest.getRequestParameters().getParameterValue(FILE_MAX_SIZE).toLong(-1);
                return fileMaxSize > 0 ? Bytes.bytes(fileMaxSize) : null;
        }
 
@@ -279,7 +357,7 @@ public abstract class AbstractFileUploadResource extends 
AbstractResource
         */
        private long getFileCountMax(ServletWebRequest webRequest)
        {
-               return 
webRequest.getRequestParameters().getParameterValue("fileCountMax").toLong(-1);
+               return 
webRequest.getRequestParameters().getParameterValue(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 c76fb7939c..1666ec5684 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
@@ -422,6 +422,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
+})();
diff --git 
a/wicket-core/src/test/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceFieldSecurityTest$UploadPage.html
 
b/wicket-core/src/test/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceFieldSecurityTest$UploadPage.html
new file mode 100644
index 0000000000..7b188cc87a
--- /dev/null
+++ 
b/wicket-core/src/test/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceFieldSecurityTest$UploadPage.html
@@ -0,0 +1,21 @@
+<!--
+   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.
+-->
+<html xmlns:wicket="http://wicket.apache.org";>
+<body>
+       <input type="file" wicket:id="upload"/>
+</body>
+</html>
diff --git 
a/wicket-core/src/test/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceFieldSecurityTest.java
 
b/wicket-core/src/test/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceFieldSecurityTest.java
new file mode 100644
index 0000000000..ec62950a7e
--- /dev/null
+++ 
b/wicket-core/src/test/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceFieldSecurityTest.java
@@ -0,0 +1,217 @@
+/*
+ * 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.assertNotEquals;
+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 javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.core.random.ISecureRandomSupplier;
+import org.apache.wicket.core.util.crypt.AESCrypt;
+import org.apache.wicket.markup.html.WebPage;
+import 
org.apache.wicket.markup.html.form.upload.resource.FileUploadToResourceField.UploadInfo;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.util.crypt.ICrypt;
+import org.apache.wicket.util.file.File;
+import org.apache.wicket.util.lang.Bytes;
+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 acceptsUploadWhenEncryptionIsNotDeterministic() throws Exception
+       {
+               SecretKey key = KeyGenerator.getInstance("AES").generateKey();
+               ISecureRandomSupplier randomSupplier = 
tester.getApplication().getSecuritySettings()
+                       .getRandomSupplier();
+               tester.getApplication().getSecuritySettings()
+                       .setCryptFactory(() -> new AESCrypt(key, 
randomSupplier));
+               ICrypt crypt = 
tester.getApplication().getSecuritySettings().getCryptFactory().newCrypt();
+               assertNotEquals(crypt.encryptUrlSafe("settings"), 
crypt.encryptUrlSafe("settings"));
+
+               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());
+       }
+
+       @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);
+               }
+       }
+}

Reply via email to