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

Reply via email to