Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New unrestricted file upload size vulnerability (#351) #454

Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ dependencies {
implementation group: 'commons-io', name: 'commons-io', version: '2.7'

implementation group: 'io.github.sasanlabs', name: 'facade-schema', version: '1.0.1'

implementation group: 'commons-fileupload', name: 'commons-fileupload', version: '1.5'
}

test {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,30 @@

import com.zaxxer.hikari.HikariDataSource;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;
import org.sasanlabs.internal.utility.LevelConstants;
import org.sasanlabs.service.vulnerability.fileupload.UnrestrictedFileUpload;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import org.springframework.web.multipart.support.MultipartFilter;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;

/**
Expand All @@ -30,6 +39,9 @@ public class VulnerableAppConfiguration {
private static final String I18N_MESSAGE_FILE_LOCATION = "classpath:i18n/messages";
private static final String ATTACK_VECTOR_PAYLOAD_PROPERTY_FILES_LOCATION_PATTERN =
"classpath:/attackvectors/*.properties";
private static final List<String> MAX_FILE_UPLOAD_SIZE_OVERRIDE_PATHS =
Arrays.asList(
"/" + UnrestrictedFileUpload.CONTROLLER_PATH + "/" + LevelConstants.LEVEL_10);

/**
* Will Inject MessageBundle into messageSource bean.
Expand Down Expand Up @@ -123,4 +135,29 @@ public JdbcTemplate applicationJdbcTemplate(
@Qualifier("applicationDataSource") DataSource applicationDataSource) {
return new JdbcTemplate(applicationDataSource);
}

/**
* Customized MultipartFilter bean disables default max upload size for multipart files and
* their overall requests, for select paths. See {@link
* UnrestrictedFileUpload#getVulnerablePayloadLevel10()} for usage.
*/
@Bean
@Order(0)
public MultipartFilter multipartFilter() {
class MaxUploadSizeOverrideMultipartFilter extends MultipartFilter {
@Override
protected MultipartResolver lookupMultipartResolver(HttpServletRequest request) {
if (MAX_FILE_UPLOAD_SIZE_OVERRIDE_PATHS.contains(request.getServletPath())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way even simple url with CONTROLLER_PATH will match this filter. Does checking other way around be better?

Copy link
Contributor Author

@tkomlodi tkomlodi Nov 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand this comment. The servlet path must exactly match
"/" + UnrestrictedFileUpload.CONTROLLER_PATH + "/" + LevelConstants.LEVEL_10
for the condition to evaluate to true.

Copy link
Member

@preetkaran20 preetkaran20 Nov 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are using here contains instead of equals and that is the reason for the question. we can make it opposite like if request.getServletPath().contains(MAX_FILE_UPLOAD_SIZE_OVERRIDE_PATHS). Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_FILE_UPLOAD_SIZE_OVERRIDE_PATHS is a list of string values. I set it up this way so later we can add to it if necessary. The check here is contains(), as it checks if there is a string in the list, which is equal to the path. In this context, this is really an "equals" check, with any of the values of the list. Which is, for now, just one possible value.
I can change this to be just a single value, but I believe, the behavior is already what you are suggesting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aauch ... My mistake.

CommonsMultipartResolver multipart = new CommonsMultipartResolver();
multipart.setMaxUploadSize(-1);
multipart.setMaxUploadSizePerFile(-1);
return multipart;
} else {
// returns default implementation
return lookupMultipartResolver();
}
}
};
return new MaxUploadSizeOverrideMultipartFilter();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -41,11 +43,13 @@
*/
@VulnerableAppRestController(
descriptionLabel = "UNRESTRICTED_FILE_UPLOAD_VULNERABILITY",
value = "UnrestrictedFileUpload")
value = UnrestrictedFileUpload.CONTROLLER_PATH)
public class UnrestrictedFileUpload {
private Path root;
private Path contentDispositionRoot;
private List<byte[]> heapMemoryFileStore = new ArrayList<>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this, shall we use the current filesystem like we are doing in all other levels?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to store large files in memory and not on the file system for two reasons:

  1. The integrity of the system that the application runs on. If these files are stored on the file system, they can cause the runtime environment to run out disk space and become corrupted. If it is a VM, it can become unusable depending on the setup. If it is a laptop, like in my case, it could also cause some serious unrecoverable issues. If the files are stored in memory, a simple application restart will reclaim the used memory without the possibility of system corruption.
  2. It takes a much smaller file size to cause a DOS condition. Usually the available memory to an application is less than available disk space. It is also easier to confirm this by simply looking at the error logs of the application server. If the disk runs out of space, it is possible that no error logs will be generated due to this.

I can make large uploaded files downloadable, if this is something desirable. One can just access these files directly from memory and stream them to the client.

With all that in mind, if you still prefer writing these files to the disk, just let me know and I'll update the code accordingly.
Thanks

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that is the case then put in the temp folder instead and then we also need to look for files in temp folder for downloading. However, for this PR i would suggest to go ahead with storing in filesystem and take temp folder related change as enhancement or in a different issue.


public static final String CONTROLLER_PATH = "UnrestrictedFileUpload";
private static final String STATIC_FILE_LOCATION = "upload";
static final String CONTENT_DISPOSITION_STATIC_FILE_LOCATION = "contentDispositionUpload";
private static final String BASE_PATH = "static";
Expand Down Expand Up @@ -92,10 +96,10 @@ public UnrestrictedFileUpload() throws IOException, URISyntaxException {
"If you are running vulnerableApp as a Jar then UnrestrictedFileUpload will not work. "
+ "For more information: https://github.com/SasanLabs/VulnerableApp/issues/255",
e);
if (root != null) {
if (root == null || !root.toFile().exists()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will we be able to retrieve the files from temp location after this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. What I aimed for and tested is that my change does not impact the original behavior. E.g. I tested the app running as a jar, and also running with ./gradlew bootRun. (I cannot use docker in my current setup.) In both cases the behavior stayed unchanged. However, I'm not 100% clear on how this is supposed to work. I don't see a way in the existing code to return files saved to a temp root location. When the root location stays a static location within the published web file structure, this gets served as all the other static files. However, when root gets modified to a tmp location, there is no way to load saved files from there. PreflightController does not have logic to serve files from a temp root.

I didn't enter a bug for this as I'm not 100% sure that I'm not misreading the code and this wasn't the focus of my work. But there might be an existing bug in there. My change should not impact this either way, the original logic should be unchanged.

root = Files.createTempDirectory(null);
}
if (contentDispositionRoot != null) {
if (contentDispositionRoot == null || !contentDispositionRoot.toFile().exists()) {
contentDispositionRoot = Files.createTempDirectory(null);
}
}
Expand Down Expand Up @@ -379,4 +383,28 @@ public ResponseEntity<GenericVulnerabilityResponseBean<String>> getVulnerablePay
true,
false);
}

@AttackVector(
vulnerabilityExposed = {
VulnerabilityType.UNCONTROLLED_RESOURCE_CONSUPTION,
VulnerabilityType.DENIAL_OF_SERVICE
},
description = "UNRESTRICTED_FILE_UPLOAD_UNCONTROLLED_RESOURCE_CONSUPTION",
payload = "UNRESTRICTED_FILE_UPLOAD_PAYLOAD_LEVEL_10")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please make this level as 9 and the current level 9 as level 10 so that we can give users a gradual experience with increasing complexity.

@VulnerableAppRequestMapping(
value = LevelConstants.LEVEL_10,
htmlTemplate = "LEVEL_1/FileUpload",
requestMethod = RequestMethod.POST)
public ResponseEntity<GenericVulnerabilityResponseBean<String>> getVulnerablePayloadLevel10(
@RequestParam(REQUEST_PARAMETER) MultipartFile file) throws IOException {
// stores files in heap memory indefinitely to allow triggering OutOfMemoryError
heapMemoryFileStore.add(file.getBytes());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest using

genericFileUploadUtility(
                root,
                RANDOM.nextInt() + "_" + file.getOriginalFilename(),
                validator,
                file,
                true,
                false);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please check the other levels for mechanism of storing files

return new ResponseEntity<GenericVulnerabilityResponseBean<String>>(
new GenericVulnerabilityResponseBean<String>("File accepted.", true),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should return the path of file instead of returning File Accepted like we are doing this for other levels.

HttpStatus.OK);
}

List<byte[]> getStoredFiles() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed.

return heapMemoryFileStore;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public enum VulnerabilityType {
COMMAND_INJECTION(77, 31),

UNRESTRICTED_FILE_UPLOAD(434, null),
UNCONTROLLED_RESOURCE_CONSUPTION(400, null),
DENIAL_OF_SERVICE(730, 10),

OPEN_REDIRECT_3XX_STATUS_CODE(601, 38),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ UNRESTRICTED_FILE_UPLOAD_PAYLOAD_LEVEL_7=Hackers can execute code in a user's ma
The extension after the null byte is ignored and the file will be treated as .html. Now if we upload this file and access it, the file should get executed in the browser.
UNRESTRICTED_FILE_UPLOAD_PAYLOAD_LEVEL_8=A path traversal attack aims to overwrite files and directories that are stored outside the web root folder. By manipulating variables that reference files with "dot-dot-slash (../)" sequences and its variations or by using absolute file paths, it may be possible to change arbitrary files and directories stored on file system including application source code or configuration and critical system files.<br/>\
<b>How to exploit this level?</b><br/>\
This level is vulnerable for path traversal, so if you choose a filename like <code>../index.html</code> then the file will be stored in another directory than provided.
This level is vulnerable for path traversal, so if you choose a filename like <code>../index.html</code> then the file will be stored in another directory than provided.
UNRESTRICTED_FILE_UPLOAD_PAYLOAD_LEVEL_10=Uploading very large files may lead to denial of service attacks on file space or other web application functions that may be impacted by in-memory file manipulation. This could overload application memory or disk space causing temporary or permanent unavailability of the application.<br/>\
<b>How to exploit this level?</b><br/>\
Since there is no file size validation at this level, you can upload very large sized files. Uploaded files are stored in memory cumulatively, which may cause the application to run out of memory and become unavailable. In such cases, a java.lang.OutOfMemoryError may be thrown and logged.
1 change: 1 addition & 0 deletions src/main/resources/i18n/messages_en_US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ Important Links:<br/>\
</ol>

#### Attack Vector Description
UNRESTRICTED_FILE_UPLOAD_UNCONTROLLED_RESOURCE_CONSUPTION=Maximum uploaded file size is not limited.
Copy link
Member

@preetkaran20 preetkaran20 Dec 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add this message.properties and if possible, you can add to other translations as well and please change key to UNRESTRICTED_FILE_UPLOAD_UNCONTROLLED_RESOURCE_CONSUMPTION.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add placeholders, but I don't speak the other languages, so can't add actual translations. I could do google translate, if that is acceptable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add just in message.properties

UNRESTRICTED_FILE_UPLOAD_NO_VALIDATION_FILE_NAME=There is no validation on uploaded file's name.
UNRESTRICTED_FILE_UPLOAD_IF_NOT_HTML_FILE_EXTENSION=All file extensions are allowed except .html extensions.
UNRESTRICTED_FILE_UPLOAD_IF_NOT_HTML_NOT_HTM_FILE_EXTENSION=All file extensions are allowed except .html and .htm extensions.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.sasanlabs.service.vulnerability.fileupload;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.net.URISyntaxException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.sasanlabs.service.vulnerability.bean.GenericVulnerabilityResponseBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;

public class UnrestrictedFileUploadTest {
private UnrestrictedFileUpload unrestrictedFileUpload;

@BeforeEach
void setUp() throws IOException, URISyntaxException {
unrestrictedFileUpload = new UnrestrictedFileUpload();
}

@Test
void unrestrictedFileSizeUploadLevel10_OverLimitFileSize_FileContentSavedInMemory()
throws Exception {
final byte[] fileContent = "Test file content".getBytes();

MultipartFile multiplartFile = Mockito.mock(MultipartFile.class);
when(multiplartFile.getBytes()).thenReturn(fileContent);

ResponseEntity<GenericVulnerabilityResponseBean<String>> result =
unrestrictedFileUpload.getVulnerablePayloadLevel10(multiplartFile);

assertEquals(HttpStatus.OK, result.getStatusCode());

assertEquals(
1,
unrestrictedFileUpload.getStoredFiles().size(),
"Uploaded file with unrestricted size is not found.");
assertEquals(
fileContent,
unrestrictedFileUpload.getStoredFiles().get(0),
"The content of uploaded file with unrestricted size is unexpected.");
}
}
Loading