Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<html xmlns:wicket="http://wicket.apache.org">
<body>
<input type="file" wicket:id="upload"/>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -81,6 +87,14 @@ public AbstractFileUploadResource(IUploadsFileManager fileManager)
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
*
Expand All @@ -101,6 +115,12 @@ protected ResourceResponse newResourceResponse(Attributes attributes)
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);
Expand Down Expand Up @@ -186,6 +206,51 @@ public void writeData(Attributes attributes) throws IOException
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)
{
Expand Down Expand Up @@ -263,7 +328,7 @@ private Bytes getMaxSize(ServletWebRequest webRequest)
{
try
{
return Bytes.bytes(getParameterValue(webRequest, "maxSize").toLong());
return Bytes.bytes(getParameterValue(webRequest, MAX_SIZE).toLong());
}
catch (StringValueConversionException e)
{
Expand All @@ -279,7 +344,7 @@ private Bytes getMaxSize(ServletWebRequest webRequest)
*/
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;
}

Expand All @@ -300,7 +365,7 @@ private String getUploadId(ServletWebRequest webRequest)
*/
private long getFileCountMax(ServletWebRequest webRequest)
{
return getParameterValue(webRequest, "fileCountMax").toLong(-1);
return getParameterValue(webRequest, FILE_COUNT_MAX).toLong(-1);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@ public void renderHead(IHeaderResponse response) {
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 + ","
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -106,4 +107,4 @@
Wicket.Log.log("Too late to cancel upload for field '" + this.inputName + "': the upload has already finished.");
}
}
})();
})();