fix: Increase supported header size for multipart data (#11058)

* Increase supported header size for multipart data to 128kB

* Update error to be thrown from the global exception handler
This commit is contained in:
Abhijeet 2022-02-11 16:56:40 +05:30 committed by GitHub
parent 44ce1d4b5a
commit d218b48cf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 165 additions and 1 deletions

View File

@ -0,0 +1,28 @@
package com.appsmith.server.configurations;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
import org.springframework.http.codec.multipart.Part;
import org.springframework.web.reactive.config.WebFluxConfigurer;
@Configuration
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer
.defaultCodecs()
.configureDefaultCodec(codec -> {
if (codec instanceof MultipartHttpMessageReader) {
HttpMessageReader<Part> partReader = ((MultipartHttpMessageReader) codec).getPartReader();
if (partReader instanceof DefaultPartHttpMessageReader) {
// Set max file part header size to 128kB
((DefaultPartHttpMessageReader) partReader).setMaxHeadersSize(128 * 1024);
}
}
});
}
}

View File

@ -136,6 +136,8 @@ public enum AppsmithError {
SSH_KEY_GENERATION_ERROR(500, 5015, "Failed to generate SSH keys, please contact Appsmith support for more details", AppsmithErrorAction.DEFAULT, null, ErrorType.GIT_CONFIGURATION_ERROR, null),
GIT_UPSTREAM_CHANGES(400, 4048, "Looks like there are pending upstream changes. To prevent you from losing history, we will pull the changes and push them to your repo.", AppsmithErrorAction.DEFAULT, null, ErrorType.GIT_ACTION_EXECUTION_ERROR, ErrorReferenceDocUrl.GIT_UPSTREAM_CHANGES),
GIT_GENERIC_ERROR(504, 5016, "Git command execution error: {0}", AppsmithErrorAction.DEFAULT, null, ErrorType.GIT_ACTION_EXECUTION_ERROR, null),
GENERIC_JSON_IMPORT_ERROR(400, 4049, "Unable to import application in organization {0} with error {1}", AppsmithErrorAction.DEFAULT, null, ErrorType.BAD_REQUEST, null),
FILE_PART_DATA_BUFFER_ERROR(500, 5017, "Failed to upload file with error: {0}", AppsmithErrorAction.DEFAULT, null, ErrorType.BAD_REQUEST, null),
;
private final Integer httpErrorCode;

View File

@ -8,6 +8,7 @@ import com.appsmith.server.dtos.ResponseDTO;
import io.sentry.Sentry;
import io.sentry.SentryLevel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.FieldError;
@ -162,6 +163,16 @@ public class GlobalExceptionHandler {
appsmithError.getMessage())));
}
@ExceptionHandler
@ResponseBody
public Mono<ResponseDTO<ErrorDTO>> catchDataBufferLimitException(DataBufferLimitException e, ServerWebExchange exchange) {
AppsmithError appsmithError = AppsmithError.FILE_PART_DATA_BUFFER_ERROR;
exchange.getResponse().setStatusCode(HttpStatus.resolve(appsmithError.getHttpErrorCode()));
doLog(e);
return Mono.just(new ResponseDTO<>(appsmithError.getHttpErrorCode(), new ErrorDTO(appsmithError.getAppErrorCode(),
appsmithError.getMessage(e.getMessage()))));
}
/**
* This function catches the generic Exception class and is meant to be a catch all to ensure that we don't leak
* any information to the client. Ideally, the function #catchAppsmithException should be used

View File

@ -498,7 +498,8 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
}.getType();
ApplicationJson jsonFile = gson.fromJson(data, fileType);
return importApplicationInOrganization(orgId, jsonFile);
});
})
.onErrorResume(error -> Mono.error(new AppsmithException(AppsmithError.GENERIC_JSON_IMPORT_ERROR, orgId, error.getMessage())));
return Mono.create(sink -> importedApplicationMono
.subscribe(sink::success, sink::error, null, sink.currentContext())

View File

@ -0,0 +1,122 @@
package com.appsmith.server.controllers;
import com.appsmith.server.configurations.SecurityTestConfig;
import com.appsmith.server.constants.Url;
import com.appsmith.server.domains.Application;
import com.appsmith.server.services.ApplicationPageService;
import com.appsmith.server.services.ApplicationService;
import com.appsmith.server.services.ThemeService;
import com.appsmith.server.solutions.ApplicationFetcher;
import com.appsmith.server.solutions.ApplicationForkingService;
import com.appsmith.server.solutions.ImportExportApplicationService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.BodyInserters;
import reactor.core.publisher.Mono;
import java.io.IOException;
@RunWith(SpringRunner.class)
@WebFluxTest(ApplicationController.class)
@Import(SecurityTestConfig.class)
public class ApplicationControllerTest {
@Autowired
private WebTestClient webTestClient;
@MockBean
ApplicationService applicationService;
@MockBean
ApplicationPageService applicationPageService;
@MockBean
ApplicationFetcher applicationFetcher;
@MockBean
ApplicationForkingService applicationForkingService;
@MockBean
ImportExportApplicationService importExportApplicationService;
@MockBean
ThemeService themeService;
private String getFileName(int length) {
StringBuilder fileName = new StringBuilder();
for (int count = 0; count < length; count++) {
fileName.append("i");
}
fileName.append(".json");
return fileName.toString();
}
private MultipartBodyBuilder createBodyBuilder(String fileName) throws IOException {
MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder();
bodyBuilder
.part("file", new ClassPathResource("test_assets/ImportExportServiceTest/invalid-json-without-app.json").getFile(), MediaType.APPLICATION_JSON)
.header("Content-Disposition", "form-data; name=\"file\"; filename=" + fileName)
.header("Content-Type", "application/json");
return bodyBuilder;
}
@Test
@WithMockUser
public void whenFileUploadedWithLongHeader_thenVerifyErrorStatus() throws IOException {
Mockito.when(importExportApplicationService.extractFileAndSaveApplication(Mockito.any(), Mockito.any()))
.thenReturn(Mono.just(new Application()));
final String fileName = getFileName(130 * 1024);
MultipartBodyBuilder bodyBuilder = createBodyBuilder(fileName);
webTestClient.post()
.uri(Url.APPLICATION_URL + "/import/orgId")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(bodyBuilder.build()))
.exchange()
.expectStatus()
.isEqualTo(500)
.expectBody()
.json("{\n" +
" \"responseMeta\": {\n" +
" \"status\": 500,\n" +
" \"success\": false,\n" +
" \"error\": {\n" +
" \"code\": 5016,\n" +
" \"message\": \"Failed to upload file with error: Part headers exceeded the memory usage limit of 131072 bytes\"\n" +
" }\n" +
" }\n" +
"}");
}
@Test
@WithMockUser
public void whenFileUploadedWithShortHeader_thenVerifySuccessStatus() throws IOException {
Mockito.when(importExportApplicationService.extractFileAndSaveApplication(Mockito.any(), Mockito.any()))
.thenReturn(Mono.just(new Application()));
final String fileName = getFileName(2 * 1024);
MultipartBodyBuilder bodyBuilder = createBodyBuilder(fileName);
webTestClient.post()
.uri(Url.APPLICATION_URL + "/import/orgId")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(bodyBuilder.build()))
.exchange()
.expectStatus()
.isEqualTo(200);
}
}