-
-
Notifications
You must be signed in to change notification settings - Fork 423
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
Changes from 2 commits
6ce0de9
daa993c
d1db58f
10dd208
b7adbc2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
||
/** | ||
|
@@ -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. | ||
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please rename as per the working of the filter. |
||
@Override | ||
protected MultipartResolver lookupMultipartResolver(HttpServletRequest request) { | ||
if (MAX_FILE_UPLOAD_SIZE_OVERRIDE_PATHS.contains(request.getServletPath())) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 CustomMF(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<byte[]> heapMemoryFileStore = new ArrayList<>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"; | ||
|
@@ -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()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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); | ||
} | ||
} | ||
|
@@ -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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest using
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should return the path of file instead of returning |
||
HttpStatus.OK); | ||
} | ||
|
||
List<byte[]> getStoredFiles() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
|
@@ -114,6 +114,7 @@ Important Links:<br/>\ | |
</ol> | ||
|
||
#### Attack Vector Description | ||
UNRESTRICTED_FILE_UPLOAD_UNCONTROLLED_RESOURCE_CONSUPTION=Maximum uploaded file size is not limited. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<GenericVulnerabilityResponseBean<String>> 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."); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we really need this? we are not using spring context in the code. I think this will make the project heavier
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For mocking multipart file, I think mockito should help by mocking the interface and it will dynamically create the implementation and that way you can return the values by using Mockito.when ... then construct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I introduced spring-test when I was trying to make the integration test work. When I moved on from that, I didn't think of simply going back to mockito.