Skip to content

Commit 824e712

Browse files
committed
Reuses logic to create temporary file
1 parent 63f2c8b commit 824e712

File tree

3 files changed

+87
-7
lines changed

3 files changed

+87
-7
lines changed

core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.apache.struts2.StrutsConstants;
3636
import org.apache.struts2.dispatcher.LocalizedMessage;
3737

38+
import java.io.File;
3839
import java.io.IOException;
3940
import java.nio.charset.Charset;
4041
import java.nio.file.Path;
@@ -44,6 +45,7 @@
4445
import java.util.HashMap;
4546
import java.util.List;
4647
import java.util.Map;
48+
import java.util.UUID;
4749

4850
import static org.apache.commons.lang3.StringUtils.normalizeSpace;
4951

@@ -406,6 +408,22 @@ public String[] getParameterValues(String name) {
406408
return values.toArray(new String[0]);
407409
}
408410

411+
/**
412+
* Creates a secure temporary file in the specified directory using UUID-based naming.
413+
* This method ensures files are created in a controlled location rather than the
414+
* system temporary directory, reducing security risks.
415+
*
416+
* @param fileName the original filename for logging purposes
417+
* @param location the directory where the temporary file should be created
418+
* @return a new temporary file in the specified location
419+
*/
420+
protected File createTemporaryFile(String fileName, Path location) {
421+
String uid = UUID.randomUUID().toString().replace("-", "_");
422+
File file = location.resolve("upload_" + uid + ".tmp").toFile();
423+
LOG.debug("Creating temporary file: {} (originally: {})", file.getName(), fileName);
424+
return file;
425+
}
426+
409427
/* (non-Javadoc)
410428
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp()
411429
*/

core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
103103
* @param saveDir the directory where uploaded files will be stored
104104
* @throws IOException if an error occurs during upload processing
105105
* @see #processNormalFormField(DiskFileItem, Charset)
106-
* @see #processFileField(DiskFileItem)
106+
* @see #processFileField(DiskFileItem, String)
107107
*/
108108
@Override
109109
protected void processUpload(HttpServletRequest request, String saveDir) throws IOException {
@@ -126,7 +126,7 @@ protected void processUpload(HttpServletRequest request, String saveDir) throws
126126
} else {
127127
// Process file upload fields
128128
LOG.debug(() -> "Processing a file: " + normalizeSpace(item.getFieldName()));
129-
processFileField(item);
129+
processFileField(item, saveDir);
130130
}
131131
}
132132
}
@@ -193,7 +193,7 @@ protected void processNormalFormField(DiskFileItem item, Charset charset) throws
193193
* @param item the disk file item representing the uploaded file
194194
* @see #cleanUpTemporaryFiles()
195195
*/
196-
protected void processFileField(DiskFileItem item) {
196+
protected void processFileField(DiskFileItem item, String saveDir) {
197197
// Skip file uploads that don't have a file name - meaning that no file was selected.
198198
if (item.getName() == null || item.getName().trim().isEmpty()) {
199199
LOG.debug(() -> "No file has been uploaded for the field: " + normalizeSpace(item.getFieldName()));
@@ -211,7 +211,7 @@ protected void processFileField(DiskFileItem item) {
211211
if (item.isInMemory()) {
212212
LOG.debug("Creating temporary file representing in-memory uploaded item: {}", normalizeSpace(item.getFieldName()));
213213
try {
214-
File tempFile = File.createTempFile("struts_upload_", "_" + item.getName());
214+
File tempFile = createTemporaryFile(item.getName(), Path.of(saveDir));
215215

216216
// Track the temporary file for explicit cleanup
217217
temporaryFiles.add(tempFile);
@@ -299,7 +299,7 @@ protected void cleanUpDiskFileItems() {
299299
*
300300
* <p>This method deletes all temporary files that were created when
301301
* processing in-memory uploads. These files are created in
302-
* {@link #processFileField(DiskFileItem)} when an uploaded file is
302+
* {@link #processFileField(DiskFileItem, String)} when an uploaded file is
303303
* stored in memory and needs to be written to disk.</p>
304304
*
305305
* <p>The cleanup process:</p>

core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequestTest.java

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import java.io.IOException;
2727
import java.lang.reflect.Field;
2828
import java.nio.charset.StandardCharsets;
29+
import java.nio.file.Files;
30+
import java.nio.file.Path;
2931
import java.util.List;
3032

3133
import static org.apache.commons.lang3.StringUtils.normalizeSpace;
@@ -112,7 +114,7 @@ public void temporaryFileCreationFailureAddsError() throws IOException {
112114
// Create a custom implementation that simulates temp file creation failure
113115
class FaultyJakartaMultiPartRequest extends JakartaMultiPartRequest {
114116
@Override
115-
protected void processFileField(DiskFileItem item) {
117+
protected void processFileField(DiskFileItem item, String saveDir) {
116118
// Simulate in-memory upload that fails to create temp file
117119
if (item.isInMemory()) {
118120
try {
@@ -127,7 +129,7 @@ protected void processFileField(DiskFileItem item) {
127129
}
128130
}
129131
} else {
130-
super.processFileField(item);
132+
super.processFileField(item, saveDir);
131133
}
132134
}
133135
}
@@ -219,4 +221,64 @@ public void endToEndMultipartProcessingWithCleanup() throws IOException {
219221
assertThat(multiPart.parameters).isEmpty();
220222
}
221223

224+
@Test
225+
public void temporaryFilesCreatedInSaveDirectory() throws IOException, NoSuchFieldException, IllegalAccessException {
226+
// Test that temporary files for in-memory uploads are created in the saveDir, not system temp
227+
String content = formFile("file1", "test1.csv", "small,content") +
228+
endline + "--" + boundary + "--";
229+
230+
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
231+
232+
// when
233+
multiPart.parse(mockRequest, tempDir);
234+
235+
// Access private field to get temporary files
236+
Field tempFilesField = JakartaMultiPartRequest.class.getDeclaredField("temporaryFiles");
237+
tempFilesField.setAccessible(true);
238+
@SuppressWarnings("unchecked")
239+
List<File> temporaryFiles = (List<File>) tempFilesField.get(multiPart);
240+
241+
// then - verify temporary files are created in saveDir
242+
assertThat(temporaryFiles).isNotEmpty();
243+
for (File tempFile : temporaryFiles) {
244+
// Verify the temporary file is in the saveDir, not system temp
245+
assertThat(tempFile.getParent()).isEqualTo(tempDir);
246+
assertThat(tempFile.getName()).startsWith("upload_");
247+
assertThat(tempFile.getName()).endsWith(".tmp");
248+
assertThat(tempFile).exists();
249+
}
250+
}
251+
252+
@Test
253+
public void secureTemporaryFileNaming() throws IOException, NoSuchFieldException, IllegalAccessException {
254+
// Test that temporary files use UUID-based naming for security
255+
String content = formFile("file1", "malicious../../../etc/passwd", "content") +
256+
endline + "--" + boundary + "--";
257+
258+
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
259+
260+
// when
261+
multiPart.parse(mockRequest, tempDir);
262+
263+
// Access private field to get temporary files
264+
Field tempFilesField = JakartaMultiPartRequest.class.getDeclaredField("temporaryFiles");
265+
tempFilesField.setAccessible(true);
266+
@SuppressWarnings("unchecked")
267+
List<File> temporaryFiles = (List<File>) tempFilesField.get(multiPart);
268+
269+
// then - verify secure naming prevents directory traversal
270+
assertThat(temporaryFiles).isNotEmpty();
271+
for (File tempFile : temporaryFiles) {
272+
// Verify the temporary file uses secure UUID naming
273+
assertThat(tempFile.getName()).startsWith("upload_");
274+
assertThat(tempFile.getName()).endsWith(".tmp");
275+
// Verify it doesn't contain malicious path elements
276+
assertThat(tempFile.getName()).doesNotContain("..");
277+
assertThat(tempFile.getName()).doesNotContain("/");
278+
assertThat(tempFile.getName()).doesNotContain("\\");
279+
// Verify it's in the correct directory
280+
assertThat(tempFile.getParent()).isEqualTo(tempDir);
281+
}
282+
}
283+
222284
}

0 commit comments

Comments
 (0)