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

Fix to reject any unintended parameters in multipart request. #5569

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

Bue-von-hon
Copy link
Contributor

Motivation:
this resolves #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.

@Bue-von-hon
Copy link
Contributor Author

I'm concerned that the behavior of pulling values from ServiceRequestContext is not clean.
Moreover, since the graphql service doesn't take parameters, I realized that I should have allowed all cases where the value is not set in the ServiceRequestContext.

@Bue-von-hon Bue-von-hon marked this pull request as draft April 5, 2024 08:24
Comment on lines 316 to 320
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);
Copy link
Member

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.

@github-actions github-actions bot added the Stale label May 10, 2024
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.
@Bue-von-hon Bue-von-hon marked this pull request as ready for review May 17, 2024 08:56
@Bue-von-hon
Copy link
Contributor Author

Resolved all comments PTAL @trustin

@Bue-von-hon
Copy link
Contributor Author

Fixed missing GPG signature.
But now I have a duplicate commit.
I'll fix it soon. 🥲

@Bue-von-hon
Copy link
Contributor Author

Fixed missing GPG signature. But now I have a duplicate commit. I'll fix it soon. 🥲

Fixed it all. 🙂

Copy link
Member

@minwoox minwoox left a 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. 😉

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)) {
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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?

@Bue-von-hon Bue-von-hon marked this pull request as draft May 22, 2024 07:33
Bue-von-hon and others added 3 commits May 27, 2024 16:14
…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.
@Bue-von-hon Bue-von-hon marked this pull request as ready for review June 19, 2024 11:19
@Bue-von-hon
Copy link
Contributor Author

Resolves all! PTAL

* @param predicate certain conditions
* @return multipart matching the condition
*/
default Multipart filterBodyParts(Predicate<? super BodyPart> predicate) {
Copy link
Member

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);
Copy link
Member

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 -> {
Copy link
Member

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()?

Comment on lines 107 to 109
final AggregatedHttpResponse response =
server.blockingWebClient().execute(multipart.toHttpRequest(path));
assertEquals(HttpStatus.OK, response.status());
Copy link
Member

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.

@Bue-von-hon Bue-von-hon marked this pull request as draft July 5, 2024 05:21
@Bue-von-hon
Copy link
Contributor Author

``I've resolved code review. @trustin @minwoox @jrhee17 @ikhoon
I hold off on renaming the filterBodyParts method of the Multipart interface.
The method named filter exists in the StreamMessage interface because the DefaultMultipart class uses the StreamMessage interface and the Multipart interface.
If we change the name of the method, it will cause a conflict, so it's better to have different names to distinguish them.
Another option is to use composition.
Implementations are always open to change, so feel free to comment if there is a better way.

by the way, multipartSingleFile and multipartFileList test broken in my local. (java/com/linecorp/armeria/graphql/GraphqlTest.java)
I think the changes I added might have had an impact, but I'm not sure.
I checked the configure method of that class, expecting to find something related to operations, but there isn't.

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)

@Bue-von-hon Bue-von-hon marked this pull request as ready for review July 15, 2024 06:02
@Bue-von-hon Bue-von-hon marked this pull request as draft August 2, 2024 01:34
@Bue-von-hon
Copy link
Contributor Author

Since the graphql api had no parameters, the tests were failing.
To fix this, I modified it to not consider the parameter if it is empty.
PTAL @trustin @minwoox @jrhee17 @ikhoon

@Bue-von-hon Bue-von-hon marked this pull request as ready for review August 2, 2024 04:39
@github-actions github-actions bot added the Stale label Sep 5, 2024
@github-actions github-actions bot removed the Stale label Sep 16, 2024
Since the request is processed and the file is deleted, change to testing with the response value
@Bue-von-hon
Copy link
Contributor Author

The file is deleted as soon as the request is processed, so the test was always succeeding.
This is now fixed by using the response value to validate.
PTAL @jrhee17 @minwoox @trustin @ikhoon

Copy link
Contributor

@jrhee17 jrhee17 left a 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 🙇

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you revert the file permission changes? ditto for the other files as well

스크린샷 2024-09-19 오후 2 51 21

* @param predicate certain conditions
* @return multipart matching the condition
*/
default Multipart filterBodyParts(Predicate<BodyPart> predicate) {
Copy link
Contributor

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

Suggested change
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()) {
Copy link
Contributor

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);
...

final AggregatedHttpResponse response =
server.blockingWebClient().execute(multipart.toHttpRequest(path));
assertEquals(HttpStatus.OK, response.status());
assertThatJson(response.contentUtf8())
Copy link
Contributor

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"));

@Bue-von-hon Bue-von-hon marked this pull request as draft October 2, 2024 11:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Make annotated services reject the multipart requests that contain an uninjectable file upload
4 participants