This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-spring-boot.git
The following commit(s) were added to refs/heads/main by this push:
new 9f358abc006 CAMEL-24496: camel-platform-http-starter - align
SpringBootPlatformHttpBinding multipart handling (#1897)
9f358abc006 is described below
commit 9f358abc006b5a12fd3157d14dfecb5713127dd9
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Aug 28 13:08:16 2026 +0200
CAMEL-24496: camel-platform-http-starter - align
SpringBootPlatformHttpBinding multipart handling (#1897)
Evaluate fileNameExtWhitelist against the submitted file name rather than
the
multipart field key, fail closed when a whitelist is configured and the
name has
no extension, and match whole comma-separated tokens instead of substrings.
Move the whitelist decision ahead of transferTo() so a rejected upload is
never
written to the servlet temp directory.
Apply FileUtil.stripPath to the value set on CamelFileName, matching the
normalisation CAMEL-24293 applied to the vertx, zipfile and tarfile paths.
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
.../springboot/SpringBootPlatformHttpBinding.java | 87 +++++++----
...ngBootPlatformHttpFileNameExtWhitelistTest.java | 167 +++++++++++++++++++++
2 files changed, 224 insertions(+), 30 deletions(-)
diff --git
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpBinding.java
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpBinding.java
index 828e76eaf3a..ac434bcda00 100644
---
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpBinding.java
+++
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpBinding.java
@@ -104,44 +104,39 @@ public class SpringBootPlatformHttpBinding extends
DefaultHttpBinding {
message.setHeader(Exchange.ATTACHMENTS_SIZE,
multipartHttpServletRequest.getFileMap().keySet().size());
multipartHttpServletRequest.getFileMap().forEach((name,
multipartFile) -> {
try {
- Path uploadedTmpFile = Paths.get(tmpFolder.getPath(),
UUID.randomUUID().toString());
- multipartFile.transferTo(uploadedTmpFile);
-
if (name != null) {
name = name.replaceAll("[\n\r\t]", "_");
}
- boolean accepted = true;
-
- if (getFileNameExtWhitelist() != null) {
- String ext = FileUtil.onlyExt(name);
- if (ext != null) {
- ext = ext.toLowerCase(Locale.US);
- if (!getFileNameExtWhitelist().equals("*") &&
!getFileNameExtWhitelist().contains(ext)) {
- accepted = false;
- }
- }
+ // the whitelist is evaluated against the submitted file
name - the value that is
+ // propagated downstream - and before the upload is
written to the servlet temp
+ // directory, so a rejected file never reaches disk
+ if
(!isFileNameExtAccepted(multipartFile.getOriginalFilename())) {
+ LOG.debug(
+ "Cannot add file as attachment: {} because the
file is not accepted according to fileNameExtWhitelist: {}",
+ name, getFileNameExtWhitelist());
+ return;
}
- if (accepted) {
- AttachmentMessage am = new
DefaultAttachmentMessage(message);
- File uploadedFile = uploadedTmpFile.toFile();
- am.addAttachment(name, new DataHandler(new
CamelFileDataSource(uploadedFile, name)));
+ Path uploadedTmpFile = Paths.get(tmpFolder.getPath(),
UUID.randomUUID().toString());
+ multipartFile.transferTo(uploadedTmpFile);
+
+ AttachmentMessage am = new
DefaultAttachmentMessage(message);
+ File uploadedFile = uploadedTmpFile.toFile();
+ am.addAttachment(name, new DataHandler(new
CamelFileDataSource(uploadedFile, name)));
- // populate body in case there is only one attachment
- if (isSingleAttachment) {
- message.setHeader(Exchange.FILE_PATH,
uploadedFile.getAbsolutePath());
- message.setHeader(Exchange.FILE_LENGTH,
multipartFile.getSize());
- message.setHeader(Exchange.FILE_NAME,
multipartFile.getOriginalFilename());
- if (multipartFile.getContentType() != null) {
- message.setHeader(Exchange.FILE_CONTENT_TYPE,
multipartFile.getContentType());
- }
- message.setBody(uploadedTmpFile);
+ // populate body in case there is only one attachment
+ if (isSingleAttachment) {
+ message.setHeader(Exchange.FILE_PATH,
uploadedFile.getAbsolutePath());
+ message.setHeader(Exchange.FILE_LENGTH,
multipartFile.getSize());
+ // FILE_NAME is a Camel control header consumed by
file/ftp producers; the raw
+ // client-supplied multipart filename may contain path
segments, so reduce it to a
+ // leaf name (CAMEL-24293)
+ message.setHeader(Exchange.FILE_NAME,
FileUtil.stripPath(multipartFile.getOriginalFilename()));
+ if (multipartFile.getContentType() != null) {
+ message.setHeader(Exchange.FILE_CONTENT_TYPE,
multipartFile.getContentType());
}
- } else {
- LOG.debug(
- "Cannot add file as attachment: {} because the
file is not accepted according to fileNameExtWhitelist: {}",
- name, getFileNameExtWhitelist());
+ message.setBody(uploadedTmpFile);
}
} catch (IOException e) {
throw new RuntimeException(e);
@@ -150,6 +145,38 @@ public class SpringBootPlatformHttpBinding extends
DefaultHttpBinding {
}
}
+ /**
+ * Whether the submitted file name is accepted according to the configured
{@code fileNameExtWhitelist}.
+ * <p/>
+ * The whitelist is a comma separated list of extensions, or {@code *} to
accept every file. When a whitelist is
+ * configured, a file name that carries no extension is not accepted.
+ *
+ * @param fileName the submitted file name, as sent by the client
+ * @return <tt>true</tt> if the file may be added as an attachment
+ */
+ private boolean isFileNameExtAccepted(String fileName) {
+ final String whitelist = getFileNameExtWhitelist();
+ if (whitelist == null) {
+ return true;
+ }
+ final String trimmedWhitelist = whitelist.trim();
+ if ("*".equals(trimmedWhitelist)) {
+ return true;
+ }
+ // keep the multi-extension semantics of FileUtil.onlyExt so a
whitelist such as "tar.gz" works
+ final String ext = FileUtil.onlyExt(fileName);
+ if (ext == null) {
+ return false;
+ }
+ final String candidate = ext.toLowerCase(Locale.US);
+ for (String allowed : trimmedWhitelist.split(",")) {
+ if (candidate.equals(allowed.trim().toLowerCase(Locale.US))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
public void setStreaming(boolean streaming) {
this.streaming = streaming;
}
diff --git
a/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpFileNameExtWhitelistTest.java
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpFileNameExtWhitelistTest.java
new file mode 100644
index 00000000000..12c4c41e96d
--- /dev/null
+++
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpFileNameExtWhitelistTest.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.platform.http.springboot;
+
+import io.restassured.RestAssured;
+import org.apache.camel.Exchange;
+import org.apache.camel.attachment.AttachmentMessage;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import
org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.web.SecurityFilterChain;
+
+import java.nio.charset.StandardCharsets;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Verifies that {@code fileNameExtWhitelist} is enforced against the
submitted file name, fails closed when the name
+ * carries no extension, and matches whole extension tokens rather than
substrings. Also covers the
+ * {@code CamelFileName} path-segment stripping aligned with CAMEL-24293.
+ */
+@EnableAutoConfiguration
+@CamelSpringBootTest
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { CamelAutoConfiguration.class,
+ SpringBootPlatformHttpFileNameExtWhitelistTest.class,
+ SpringBootPlatformHttpFileNameExtWhitelistTest.TestConfiguration.class,
+ PlatformHttpComponentAutoConfiguration.class,
SpringBootPlatformHttpAutoConfiguration.class })
+public class SpringBootPlatformHttpFileNameExtWhitelistTest {
+
+ private static final byte[] CONTENT = "upload
content".getBytes(StandardCharsets.UTF_8);
+
+ @Autowired
+ private Environment env;
+
+ @BeforeEach
+ void setUp() {
+ RestAssured.port = env.getRequiredProperty("local.server.port",
Integer.class);
+ }
+
+ @Test
+ void acceptsWhitelistedExtension() {
+ given().multiPart("file", "invoice.pdf", CONTENT)
+ .post("/upload")
+ .then()
+ .statusCode(200)
+ .header("attachmentCount", is("1"));
+ }
+
+ @Test
+ void acceptsSecondWhitelistEntryIgnoringSurroundingSpaces() {
+ given().multiPart("file", "notes.txt", CONTENT)
+ .post("/upload")
+ .then()
+ .statusCode(200)
+ .header("attachmentCount", is("1"));
+ }
+
+ @Test
+ void rejectsExtensionOutsideTheWhitelist() {
+ given().multiPart("file", "shell.jsp", CONTENT)
+ .post("/upload")
+ .then()
+ .statusCode(200)
+ .header("attachmentCount", is("0"));
+ }
+
+ /**
+ * The whitelist must be evaluated against the submitted file name, not
the multipart field name - the field name
+ * is not the value propagated downstream.
+ */
+ @Test
+ void rejectsWhenOnlyTheFieldNameLooksWhitelisted() {
+ given().multiPart("report.pdf", "shell.jsp", CONTENT)
+ .post("/upload")
+ .then()
+ .statusCode(200)
+ .header("attachmentCount", is("0"));
+ }
+
+ /**
+ * A file name with no extension must fail closed while a whitelist is
configured.
+ */
+ @Test
+ void rejectsFileNameWithoutExtension() {
+ given().multiPart("file", "shell", CONTENT)
+ .post("/upload")
+ .then()
+ .statusCode(200)
+ .header("attachmentCount", is("0"));
+ }
+
+ /**
+ * "pd" must not be accepted just because it is a substring of the
whitelisted "pdf".
+ */
+ @Test
+ void rejectsSubstringOfAWhitelistedExtension() {
+ given().multiPart("file", "invoice.pd", CONTENT)
+ .post("/upload")
+ .then()
+ .statusCode(200)
+ .header("attachmentCount", is("0"));
+ }
+
+ @Test
+ void stripsPathSegmentsFromCamelFileName() {
+ given().multiPart("file", "../../evil.pdf", CONTENT)
+ .post("/upload")
+ .then()
+ .statusCode(200)
+ .header("attachmentCount", is("1"))
+ .header("uploadFileName", is("evil.pdf"));
+ }
+
+ @Configuration
+ public static class TestConfiguration {
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
+ http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
+ .csrf(AbstractHttpConfigurer::disable);
+ return http.build();
+ }
+
+ @Bean
+ public RouteBuilder whitelistRoute() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("platform-http:/upload?fileNameExtWhitelist=pdf,txt")
+ .process(exchange -> {
+ AttachmentMessage am =
exchange.getMessage(AttachmentMessage.class);
+ int count = am.getAttachments() == null ? 0 :
am.getAttachments().size();
+ Object fileName =
exchange.getMessage().getHeader(Exchange.FILE_NAME);
+
exchange.getMessage().setHeader("attachmentCount", String.valueOf(count));
+
exchange.getMessage().setHeader("uploadFileName", fileName == null ? "" :
fileName.toString());
+ exchange.getMessage().setBody("ok");
+ });
+ }
+ };
+ }
+ }
+}