diff --git a/build.gradle b/build.gradle index 3a93c844..4fd9806e 100644 --- a/build.gradle +++ b/build.gradle @@ -129,6 +129,8 @@ dependencies { // https://mvnrepository.com/artifact/org.assertj/assertj-core testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.17.2' + testImplementation group: 'org.springframework', name: 'spring-test' + // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '2.3.1.RELEASE' @@ -149,6 +151,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 { diff --git a/src/main/java/org/sasanlabs/configuration/VulnerableAppConfiguration.java b/src/main/java/org/sasanlabs/configuration/VulnerableAppConfiguration.java index ea8f1eed..e91aca22 100755 --- a/src/main/java/org/sasanlabs/configuration/VulnerableAppConfiguration.java +++ b/src/main/java/org/sasanlabs/configuration/VulnerableAppConfiguration.java @@ -2,9 +2,14 @@ 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; @@ -12,11 +17,15 @@ 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; /** @@ -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 MAX_FILE_UPLOAD_SIZE_OVERRIDE_PATHS = + Arrays.asList( + "/" + UnrestrictedFileUpload.CONTROLLER_PATH + "/" + LevelConstants.LEVEL_10); /** * Will Inject MessageBundle into messageSource bean. @@ -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 CustomMF extends MultipartFilter { + @Override + protected MultipartResolver lookupMultipartResolver(HttpServletRequest request) { + if (MAX_FILE_UPLOAD_SIZE_OVERRIDE_PATHS.contains(request.getServletPath())) { + CommonsMultipartResolver multipart = new CommonsMultipartResolver(); + multipart.setMaxUploadSize(-1); + multipart.setMaxUploadSizePerFile(-1); + return multipart; + } else { + // returns default implementation + return lookupMultipartResolver(); + } + } + }; + return new CustomMF(); + } } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java index adb5f95d..965afc56 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUpload.java @@ -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; @@ -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 heapMemoryFileStore = new ArrayList<>(); + 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"; @@ -379,4 +383,28 @@ public ResponseEntity> 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") + @VulnerableAppRequestMapping( + value = LevelConstants.LEVEL_10, + htmlTemplate = "LEVEL_1/FileUpload", + requestMethod = RequestMethod.POST) + public ResponseEntity> getVulnerablePayloadLevel10( + @RequestParam(REQUEST_PARAMETER) MultipartFile file) throws IOException { + // stores files in heap memory indefinitely to allow triggering OutOfMemoryError + heapMemoryFileStore.add(file.getBytes()); + return new ResponseEntity>( + new GenericVulnerabilityResponseBean("File accepted.", true), + HttpStatus.OK); + } + + List getStoredFiles() { + return heapMemoryFileStore; + } } diff --git a/src/main/java/org/sasanlabs/vulnerability/types/VulnerabilityType.java b/src/main/java/org/sasanlabs/vulnerability/types/VulnerabilityType.java index 32c428a3..56006b3a 100644 --- a/src/main/java/org/sasanlabs/vulnerability/types/VulnerabilityType.java +++ b/src/main/java/org/sasanlabs/vulnerability/types/VulnerabilityType.java @@ -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), diff --git a/src/main/resources/attackvectors/UnrestrictedFileUploadPayload.properties b/src/main/resources/attackvectors/UnrestrictedFileUploadPayload.properties index 4e107bb7..14c54280 100644 --- a/src/main/resources/attackvectors/UnrestrictedFileUploadPayload.properties +++ b/src/main/resources/attackvectors/UnrestrictedFileUploadPayload.properties @@ -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.
\ How to exploit this level?
\ - This level is vulnerable for path traversal, so if you choose a filename like ../index.html then the file will be stored in another directory than provided. \ No newline at end of file + This level is vulnerable for path traversal, so if you choose a filename like ../index.html 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.
\ +How to exploit this level?
\ + 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. diff --git a/src/main/resources/i18n/messages_en_US.properties b/src/main/resources/i18n/messages_en_US.properties index 288057e8..8192b5a7 100755 --- a/src/main/resources/i18n/messages_en_US.properties +++ b/src/main/resources/i18n/messages_en_US.properties @@ -114,6 +114,7 @@ Important Links:
\ #### Attack Vector Description +UNRESTRICTED_FILE_UPLOAD_UNCONTROLLED_RESOURCE_CONSUPTION=Maximum uploaded file size is not limited. 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. diff --git a/src/test/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUploadTest.java b/src/test/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUploadTest.java new file mode 100644 index 00000000..6adda4ab --- /dev/null +++ b/src/test/java/org/sasanlabs/service/vulnerability/fileupload/UnrestrictedFileUploadTest.java @@ -0,0 +1,42 @@ +package org.sasanlabs.service.vulnerability.fileupload; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.net.URISyntaxException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.sasanlabs.service.vulnerability.bean.GenericVulnerabilityResponseBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockMultipartFile; + +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(); + MockMultipartFile m = + new MockMultipartFile("file", "file.txt", MediaType.TEXT_PLAIN_VALUE, fileContent); + ResponseEntity> result = + unrestrictedFileUpload.getVulnerablePayloadLevel10(m); + 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."); + } +}