-
Notifications
You must be signed in to change notification settings - Fork 919
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
Fix to reject any unintended parameters in multipart request. #5569
base: main
Are you sure you want to change the base?
Conversation
I'm concerned that the behavior of pulling values from ServiceRequestContext is not clean. |
final AttributeKey<List<String>> PARAM_LIST_KEY = AttributeKey.valueOf(List.class, "names"); | ||
final List<String> names = resolvers.stream() | ||
.map(AnnotatedValueResolver::httpElementName) | ||
.collect(Collectors.toList()); | ||
ctx.setAttr(PARAM_LIST_KEY, names); |
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.
- We could just pass the param list as an additional parameter of
aggregateMultipart()
. - We could collect this information when creating an
AnnotatedService
instance, not every time service each request.
Motivation: this resolves line#5549 Modifications: - Fix(AnnotatedServiceMultipartTest): Add test for upload multipart file with unexpected parameters in AnnotatedServiceMultipartTest. - Fix(AnnotatedService): Fix to include a list of intended parameters in the ServiceRequestContext. - Fix(FileAggregatedMultipart): Fix to check if any variables are passed in the list of intended parameters and throw an acceptance if not. Result: Multipart requests with unintended parameters will no longer create files.
Resolved all comments PTAL @trustin |
Fixed missing GPG signature. |
Fixed it all. 🙂 |
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.
Thanks! I left my opinion. 😉
core/src/main/java/com/linecorp/armeria/internal/server/annotation/DefaultAnnotatedService.java
Outdated
Show resolved
Hide resolved
final Path destination = ctx.config().multipartUploadsLocation(); | ||
return Multipart.from(req).collect(bodyPart -> { | ||
final String name = bodyPart.name(); | ||
assert name != null; | ||
if (!parameters.isEmpty() && !parameters.contains(name)) { |
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.
I prefer to silently discard it because it might already process all the necessary files.
Let's say that there are 10 files and the last one is the one that the service doesnt' need.
This will reject the request after processing all the necessary files.
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.
Rather than rejecting it by throwing an exception (the loud way), I'll modify it to ignore unintended files (the quiet way)
📝 leaves memo for myself to remember later.
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.
I think it's fine to discard them as long as we don't write them into filesystem. Do we?
…tion/DefaultAnnotatedService.java Co-authored-by: minux <[email protected]>
Motivation: If an unintended file was entered, it threw an exception and terminated. This behavior was frustrating to the user, so changed to a calmer method that simply ignores the file. Modification: - Fix(AnnotatedServiceMultipartTest): Change from BAD REQUEST to OK, and change the file name to be more distinguishable. - Fix(FileAggregatedMultipart): Add filtering - Fix(Multipart): Add filtering. I wanted to name the filtering method filter, but because the StreamMessage interface already has a filter function, and because the DefaultMultipart class implements both the multipart interface and the StreamMessage interface, I used a different name. Result: It no longer raises an acceptance. Instead, unintended files are filtered out before they are written to disk.
Resolves all! PTAL |
* @param predicate certain conditions | ||
* @return multipart matching the condition | ||
*/ | ||
default Multipart filterBodyParts(Predicate<? super BodyPart> predicate) { |
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.
How about renaming to filter()
?
private static boolean shouldSkip(BodyPart bodyPart, List<String> parameters) { | ||
final String name = bodyPart.name(); | ||
assert name != null; | ||
return !parameters.isEmpty() && !parameters.contains(name); |
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.
Maybe I misunderstood, but should we return true
if parameters
is empty?
final Path destination = ctx.config().multipartUploadsLocation(); | ||
return Multipart.from(req).collect(bodyPart -> { | ||
return Multipart.from(req) | ||
.filterBodyParts(bodyPart -> !shouldSkip(bodyPart, parameters)).collect(bodyPart -> { |
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.
If we always negate the result of shouldSkip()
, we could maybe replace !shouldSkip()
with shouldHandle()
?
final AggregatedHttpResponse response = | ||
server.blockingWebClient().execute(multipart.toHttpRequest(path)); | ||
assertEquals(HttpStatus.OK, response.status()); |
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.
This doesn't make sure the multipartFile3
has been either discarded or processed at all. What about writing a new endpoint in MyAnnotatedService
that makes sure the file is not created for multipartFile3
in the directory that contains multipartFile1
and multipartFile2
.
10773e9
to
2543a9c
Compare
``I've resolved code review. @trustin @minwoox @jrhee17 @ikhoon by the way, multipartSingleFile and multipartFileList test broken in my local. (java/com/linecorp/armeria/graphql/GraphqlTest.java) GraphqlTest > multipartFileList() FAILED
org.springframework.web.client.HttpClientErrorException$BadRequest: 400 Bad Request: "'operations' form field is missing"
at app//org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:103)
at app//org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:183)
at app//org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:137)
at app//org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)
at app//org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:942)
at app//org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:891)
at app//org.springframework.web.client.RestTemplate.execute(RestTemplate.java:830)
at app//org.springframework.web.client.RestTemplate.postForEntity(RestTemplate.java:556)
at app//com.linecorp.armeria.graphql.GraphqlTest.multipartFileList(GraphqlTest.java:135)
GraphqlTest > multipartSingleFile() FAILED
org.springframework.web.client.HttpClientErrorException$BadRequest: 400 Bad Request: "'operations' form field is missing"
at app//org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:103)
at app//org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:183)
at app//org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:137)
at app//org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)
at app//org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:942)
at app//org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:891)
at app//org.springframework.web.client.RestTemplate.execute(RestTemplate.java:830)
at app//org.springframework.web.client.RestTemplate.postForEntity(RestTemplate.java:556)
at app//com.linecorp.armeria.graphql.GraphqlTest.multipartSingleFile(GraphqlTest.java:116) |
Since the request is processed and the file is deleted, change to testing with the response value
…into armeria-5549
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.
Looks good overall, left some minor suggestions 🙇
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.
* @param predicate certain conditions | ||
* @return multipart matching the condition | ||
*/ | ||
default Multipart filterBodyParts(Predicate<BodyPart> predicate) { |
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.
I think it's more intuitive to stick with names often used in fp
default Multipart filterBodyParts(Predicate<BodyPart> predicate) { | |
default Multipart filter(Predicate<BodyPart> predicate) { |
private static boolean shouldHandle(BodyPart bodyPart, List<String> parameters) { | ||
final String name = bodyPart.name(); | ||
assert name != null; | ||
if (parameters.isEmpty()) { |
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.
If an annotated service doesn't have any parameters, I think we'll run into the same issue.
What do you think of just accepting a Multipart
to avoid ambiguity?
e.g.
public static CompletableFuture<FileAggregatedMultipart> aggregateMultipart(
ServiceRequestContext ctx, HttpRequest req) {
final Multipart multipart = Multipart.from(req);
return aggregateMultipart(ctx, multipart);
}
public static CompletableFuture<FileAggregatedMultipart> aggregateMultipart(
ServiceRequestContext ctx, Multipart multipart) {
...
and then, we can pass in the filtered Multipart
from AnnotatedService
...
private CompletionStage<HttpResponse> serve1(ServiceRequestContext ctx, HttpRequest req,
AggregationType aggregationType) {
final CompletableFuture<AggregatedResult> f;
switch (aggregationType) {
case MULTIPART:
f = FileAggregatedMultipart.aggregateMultipart(ctx, Multipart.from(req).filter(bodyPart -> shouldHandle(bodyPart, parameters)))
.thenApply(AggregatedResult::new);
...
...test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedServiceMultipartTest.java
Outdated
Show resolved
Hide resolved
final AggregatedHttpResponse response = | ||
server.blockingWebClient().execute(multipart.toHttpRequest(path)); | ||
assertEquals(HttpStatus.OK, response.status()); | ||
assertThatJson(response.contentUtf8()) |
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.
In addition to checking the response, can we also check that the file has not been physically written?
e.g.
Setting the strategy to never remove automatically:
@TempDir
static java.nio.file.Path sharedTempDir;
@RegisterExtension
static final ServerExtension server = new ServerExtension() {
@Override
protected void configure(ServerBuilder sb) throws Exception {
sb.annotatedService("/", new MyAnnotatedService());
sb.decorator(LoggingService.newDecorator());
sb.multipartRemovalStrategy(MultipartRemovalStrategy.NEVER);
sb.multipartUploadsLocation(sharedTempDir);
and then, check if any of the temporary files contains "qux3"
assertThat(sharedTempDir.resolve("complete").toFile().listFiles())
.noneSatisfy(file -> assertThat(file).content(StandardCharsets.UTF_8).isEqualTo("qux3"));
…tion/AnnotatedServiceMultipartTest.java Co-authored-by: jrhee17 <[email protected]>
Motivation:
this resolves #5549
Modifications:
Result:
Multipart requests with unintended parameters will no longer create files.