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

[WW-5388] Uses the latest JakartaEE FileUpload Servlet 6 package #861

Merged
merged 11 commits into from
Feb 1, 2024

Conversation

lukaszlenart
Copy link
Member

Closes WW-5388

Also refactors the Jakarta based parsers as they have a lot in common
@lukaszlenart lukaszlenart mentioned this pull request Jan 24, 2024
@lukaszlenart lukaszlenart force-pushed the feature/WW-5388-upload-servlet6 branch 4 times, most recently from ad2f820 to 1f9e0cd Compare January 27, 2024 08:15
LOG.debug("No file has been uploaded for the field: {}", sanitizeNewlines(item.getFieldName()));
return;
}
LOG.debug("Using charset: {} or default: {}", charset, defaultEncoding);

Check notice

Code scanning / SonarCloud

Logging should not be vulnerable to injection attacks

<!--SONAR_ISSUE_KEY:AY1KAqt48H-R3IcxNTE6-->Change this code to not log user-controlled data. <p>See more on <a href="https://sonarcloud.io/project/issues?id=apache_struts&issues=AY1KAqt48H-R3IcxNTE6&open=AY1KAqt48H-R3IcxNTE6&pullRequest=861">SonarCloud</a></p>
// if the requestSizePermitted check failed based on the
// complete content-size of the request.
else {
LOG.debug("Using charset: {} or default: {}", charset, defaultEncoding);

Check notice

Code scanning / SonarCloud

Logging should not be vulnerable to injection attacks

<!--SONAR_ISSUE_KEY:AY1KAquc8H-R3IcxNTE7-->Change this code to not log user-controlled data. <p>See more on <a href="https://sonarcloud.io/project/issues?id=apache_struts&issues=AY1KAquc8H-R3IcxNTE7&open=AY1KAquc8H-R3IcxNTE7&pullRequest=861">SonarCloud</a></p>
@lukaszlenart lukaszlenart force-pushed the feature/WW-5388-upload-servlet6 branch 2 times, most recently from 4601311 to 0eead10 Compare January 27, 2024 09:26
@lukaszlenart lukaszlenart force-pushed the feature/WW-5388-upload-servlet6 branch from 0eead10 to 3294ed0 Compare January 27, 2024 10:20
@lukaszlenart lukaszlenart marked this pull request as ready for review January 27, 2024 11:00
@lukaszlenart lukaszlenart requested review from sepe81, rgielen, aleksandr-m, yasserzamani, sdutry and kusalk and removed request for sepe81 January 27, 2024 11:00
@lukaszlenart
Copy link
Member Author

/cc: @burtonrhodes

@lukaszlenart
Copy link
Member Author

/cc: @deki

@burtonrhodes
Copy link
Contributor

@lukaszlenart Looks good. The current version of this PR works well in my initial testing (dev) environment.

errorMessage = buildErrorMessage(e, new Object[]{
ex.getContentType()
});
for (DiskFileItem item : servletFileUpload.parseRequest(request)) {
Copy link
Contributor

@burtonrhodes burtonrhodes Jan 27, 2024

Choose a reason for hiding this comment

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

@lukaszlenart Quick thought on the maxFiles setting: I still think it makes more sense that maxFiles would apply to only file fields and not all form fields in the request. However it appears that this logic is actually baked into the JakartaServletFileUpload class itself when calling servletFileUpload.parseRequest(). Wonder why they coded it this way??

Anyway, if we ever wanted to change this, you could set the maxFiles to unlimited in the servletFileUpload object, and then perform the files count check in the if (item.isFormField()... logic below. Just a thought.

Copy link
Contributor

@burtonrhodes burtonrhodes Jan 27, 2024

Choose a reason for hiding this comment

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

In thinking about it more, that might be a security issue, as the serletFileUpload object would upload everything first (something that the setting is trying to prevent), only to perform the check after the processing is done. However, the maxSize / maxFileSize might offset that security risk??

Copy link
Member Author

Choose a reason for hiding this comment

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

I assume so, this can be changed with a proper implementation of Streaming API which can then calculate the max size of uploaded files internally in Struts ... hm or maybe even right now, let me think this through 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

@burtonrhodes introduced a new constant maxSizeOfFiles which controls how many files you can upload based on their size - this works with JakartaStreamMultiPartRequest only.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah this is by design (see this comment), none of the other options mitigate against this as the risk is processing a very large amount of request parts, not necessarily large ones. Though we can probably raise our default limit from 256 to 1000 (used in latest Tomcat).

@lukaszlenart lukaszlenart force-pushed the feature/WW-5388-upload-servlet6 branch from 9c4c860 to 51bf8de Compare January 28, 2024 08:47
@lukaszlenart lukaszlenart force-pushed the feature/WW-5388-upload-servlet6 branch 6 times, most recently from 9f2e912 to 2155d17 Compare January 28, 2024 13:20
@@ -27,14 +27,14 @@
* The {@link org.apache.struts2.interceptor.ActionFileUploadInterceptor} will use the interface
* to notify action about the multiple uploaded files.
*/
public interface UploadedFilesAware {
public interface UploadedFilesAware<T> {
Copy link
Member

Choose a reason for hiding this comment

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

Not sure I understand the benefit of this generic type given this interface is coupled with the ActionFileUploadInterceptor. In what situation would an action implement UploadedFilesAware<NotFile>?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a preparation to support InputStream and allow to stream files directly into actions, yet I need to thinks this through as this change became huge :\

Copy link
Member

Choose a reason for hiding this comment

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

Oh okay - yeah I just don't know if it makes sense for a marker interface to have a generic type - might just need 2 marker interfaces instead

Copy link
Member Author

Choose a reason for hiding this comment

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

Reverted

@@ -142,18 +142,25 @@ public final class StrutsConstants {
/** The maximum size of a multipart request (file upload) */
public static final String STRUTS_MULTIPART_MAXSIZE = "struts.multipart.maxSize";

/** The maximum size of all uploaded files.
Used only with {@link org.apache.struts2.dispatcher.multipart.JakartaStreamMultiPartRequest} */
public static final String STRUTS_MULTIPART_MAXSIZE_OF_FILES = "struts.multipart.maxSizeOfFiles";
Copy link
Member

Choose a reason for hiding this comment

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

Still not very consistent - shouldn't it be MAX_SIZE_OF_FILES?

Copy link
Member Author

Choose a reason for hiding this comment

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

IDEA doesn't complain about MAXSIZE that's why I left it, but you are right :)

errorMessage = buildErrorMessage(e, new Object[]{
ex.getContentType()
});
for (DiskFileItem item : servletFileUpload.parseRequest(request)) {
Copy link
Member

Choose a reason for hiding this comment

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

Yeah this is by design (see this comment), none of the other options mitigate against this as the risk is processing a very large amount of request parts, not necessarily large ones. Though we can probably raise our default limit from 256 to 1000 (used in latest Tomcat).

LOG.warn("Failed to delete '{}' due to security exception above.", file.getName(), se);
}
}
Long currentFilesSize = actualSizeOfUploadedFiles();
Copy link
Member

Choose a reason for hiding this comment

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

This seems expensive to compute from scratch every time a file field is encountered, especially when the application might not have even configured a maxSizeOfFiles limit!

Copy link
Member

Choose a reason for hiding this comment

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

Tbh I'm not convinced this option adds much value in the first place - I think maxSize serves this need well enough. I'm trying to think in what situation would an application need to configure maxSizeOfFiles instead of or in addition to maxSize. They want to accepts lots of big form fields but want to limit the amount of data written to disk? I guess it's possible

Copy link
Member Author

Choose a reason for hiding this comment

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

This seems expensive to compute from scratch every time a file field is encountered, especially when the application might not have even configured a maxSizeOfFiles limit!

Fixed!

@lukaszlenart lukaszlenart force-pushed the feature/WW-5388-upload-servlet6 branch 3 times, most recently from 5ca2764 to 61ba63b Compare January 29, 2024 17:50
@lukaszlenart lukaszlenart force-pushed the feature/WW-5388-upload-servlet6 branch from 61ba63b to e2215c8 Compare January 29, 2024 18:09
Copy link
Member

@kusalk kusalk left a comment

Choose a reason for hiding this comment

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

Lot of refactoring here so tricky to follow all the changes, but looks good at a glance

LOG.warn("Failed to delete '{}' due to security exception above.", file.getName(), se);
}
}
Long currentFilesSize = maxSizeOfFiles != null ? actualSizeOfUploadedFiles() : null;
Copy link
Member

Choose a reason for hiding this comment

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

You can just inline this right?
if (maxSizeOfFiles != null && actualSizeOfUploadedFiles() + file.length() >= maxSizeOfFiles) {

Copy link
Member Author

Choose a reason for hiding this comment

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

Not really as it is used inside the if clause in exceedsMaxSizeOfFiles

Copy link
Member

Choose a reason for hiding this comment

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

Missed that, all good :)

@burtonrhodes
Copy link
Contributor

burtonrhodes commented Jan 29, 2024

Just FYI, I implemented this PR on a small subset of my server cluster and have been receiving the following ambiguous errors in some of the upload requests. I can't duplicate it in any testing environment, but curious if you might have a clue as to why Struts is creating an actionError with no value in the error, instead it prints "{0}". For this implementation, I am not using the stream parser

Error printed by the code below

Unidentified ActionError present for file upload: [Error uploading: {0}!, Error uploading: {0}!]

Code that quickly checks for any file upload errors

if (hasActionErrors()) {
    List<String> uploadErrors = (List<String>) getActionErrors();
    log.error("Unidentified ActionError present for file upload: " + uploadErrors);
}

A redacted version of the action

public class UploadLocalFileJsonAction extends ActionSupport implements Validateable, UploadedFilesAware<UploadedFile<File>> {

    private FileAttachmentUpload fileAttachmentUpload;

    @Override
    public void withUploadedFiles(List<UploadedFile<UploadedFile<File>>> uploadedFiles) {
        if (!uploadedFiles.isEmpty()) {
            fileAttachmentUpload = UploadedFileHelper.createFromUploadedFile(uploadedFiles.get(0));
        }
    }

    public void validate() {
        // First check to see if there are any errors from the file upload
        if (hasActionErrors()) {
            List<String> uploadErrors = (List<String>) getActionErrors();
            log.error("Unidentified ActionError present for file upload: " + uploadErrors);
        }
    }

    public String execute() throws Exception {
        // Omitted for brevity
    }
}

@lukaszlenart
Copy link
Member Author

@kusalk sorry for that, I know it's a big change yet I wanted to fix all the issues I have discovered around Jakarta file upload at once. Also all the changes related to UploadedFile are mean to support other approaches as struts2-gea-plugin.

@lukaszlenart
Copy link
Member Author

@burtonrhodes I tried to play with the Showcase app and right now there is a problem uploading large files - I get blank page, yet the app is using the new Action Upload Interceptor

@lukaszlenart lukaszlenart force-pushed the feature/WW-5388-upload-servlet6 branch from 94cec3b to 18e8c94 Compare January 30, 2024 08:46
@lukaszlenart lukaszlenart force-pushed the feature/WW-5388-upload-servlet6 branch from 18e8c94 to d024ccd Compare January 30, 2024 08:53
Copy link

Quality Gate Passed Quality Gate passed

The SonarCloud Quality Gate passed, but some issues were introduced.

1 New issue
0 Security Hotspots
87.2% Coverage on New Code
0.0% Duplication on New Code

See analysis details on SonarCloud

@lukaszlenart
Copy link
Member Author

@burtonrhodes did you have a chance to re-test the latest changes? Everything works fine when testing with the Showcase app yet it uses the new action upload interceptor, but still the same MultiPartParser is used behind the scene. I even added a dedicated integration test to cover max files constraint.

@burtonrhodes
Copy link
Contributor

@lukaszlenart yes, all looks good on my end.

@lukaszlenart
Copy link
Member Author

LGTM 💯

@lukaszlenart lukaszlenart merged commit 84d350d into release/struts-7-0-x Feb 1, 2024
7 checks passed
@lukaszlenart lukaszlenart deleted the feature/WW-5388-upload-servlet6 branch February 1, 2024 08:23
*/
public String[] getParameterValues(String name) {
return parameters.getOrDefault(name, Collections.emptyList())
.toArray(String[]::new);
Copy link

Choose a reason for hiding this comment

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

This change broke query parameters in multipart requests. Their values are being lost. The old implementation returned null if the parameter didn't exist, but it now returns an empty array. MultiPartRequestWrapper::getParameterValues relies on the fact that it returns null, though, so it's not working correctly anymore.

This is the old implementation:

public String[] getParameterValues(String name) {
	List<String> v = params.get(name);
	if (v != null && !v.isEmpty()) {
		return v.toArray(new String[0]);
	}

	return null;
}

And this is the MultiPartRequestWrapper method that doesn't work correctly after the change:

public String[] getParameterValues(String name) {
	return ((multi == null) || (multi.getParameterValues(name) == null)) ? super.getParameterValues(name) : multi.getParameterValues(name);
}

Copy link
Member Author

Choose a reason for hiding this comment

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

@criderp please register this bug in JIRA, thanks!

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.

5 participants