This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.18.x by this push:
new 6d0751b1a7ae CAMEL-24650, CAMEL-24652: camel-http-common,
camel-platform-http-vertx - match fileNameExtWhitelist entries exactly (#27268)
6d0751b1a7ae is described below
commit 6d0751b1a7ae9f5bffb4365640fb414960d0fc00
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Oct 2 11:35:16 2026 +0200
CAMEL-24650, CAMEL-24652: camel-http-common, camel-platform-http-vertx -
match fileNameExtWhitelist entries exactly (#27268)
Backport of #26872 and #27194 to camel-4.18.x. fileNameExtWhitelist entries
are now compared exactly and case-insensitively through the shared
HttpHelper.isFileNameExtWhitelisted check, in VertxPlatformHttpConsumer and
DefaultHttpBinding.populateAttachments, instead of being matched as a
substring. The configured value is no longer modified.
Co-authored-by: Claude Opus 4.8 <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../org/apache/camel/http/base/HttpHelper.java | 35 +++++++++
.../base/HttpHelperFileNameExtWhitelistTest.java | 84 ++++++++++++++++++++++
.../camel/http/common/DefaultHttpBinding.java | 31 ++++----
.../camel/http/common/DefaultHttpBindingTest.java | 67 +++++++++++++++++
.../component/jetty12/AttachmentHttpBinding.java | 26 -------
components/camel-platform-http-vertx/pom.xml | 4 ++
.../http/vertx/VertxPlatformHttpConsumer.java | 18 ++---
.../component/servlet/AttachmentHttpBinding.java | 23 +-----
8 files changed, 215 insertions(+), 73 deletions(-)
diff --git
a/components/camel-http-base/src/main/java/org/apache/camel/http/base/HttpHelper.java
b/components/camel-http-base/src/main/java/org/apache/camel/http/base/HttpHelper.java
index 1a9198796eb1..7bfa7ad396c9 100644
---
a/components/camel-http-base/src/main/java/org/apache/camel/http/base/HttpHelper.java
+++
b/components/camel-http-base/src/main/java/org/apache/camel/http/base/HttpHelper.java
@@ -19,6 +19,7 @@ package org.apache.camel.http.base;
import java.net.ProtocolException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.function.BiConsumer;
@@ -26,6 +27,7 @@ import org.apache.camel.Exchange;
import org.apache.camel.ExchangePropertyKey;
import org.apache.camel.support.http.HttpUtil;
import org.apache.camel.util.CollectionHelper;
+import org.apache.camel.util.FileUtil;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.ObjectHelper;
@@ -175,4 +177,37 @@ public final class HttpHelper {
}
}
+ /**
+ * Whether an uploaded file is accepted according to a {@code
fileNameExtWhitelist}.
+ * <p/>
+ * The file name extension is compared, case-insensitively, against each
comma-separated entry of the whitelist
+ * exactly and not as a substring: a whitelist of {@code txt} must not
accept an upload named {@code evil.x} just
+ * because {@code "txt".contains("x")}. A file is accepted when no
whitelist is configured, when the whitelist is
+ * {@code *}, or when the file name has no extension. The configured
whitelist value is not modified.
+ *
+ * @param whitelist the configured {@code fileNameExtWhitelist} (may be
{@code null})
+ * @param fileName the file name submitted by the client
+ * @return true if the file is accepted
+ */
+ public static boolean isFileNameExtWhitelisted(String whitelist, String
fileName) {
+ if (whitelist == null) {
+ return true;
+ }
+ String ext = FileUtil.onlyExt(fileName);
+ if (ext == null) {
+ return true;
+ }
+ ext = ext.toLowerCase(Locale.US);
+ String lcWhitelist = whitelist.toLowerCase(Locale.US);
+ if (lcWhitelist.equals("*")) {
+ return true;
+ }
+ for (String allowed : lcWhitelist.split(",")) {
+ if (allowed.trim().equals(ext)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
}
diff --git
a/components/camel-http-base/src/test/java/org/apache/camel/http/base/HttpHelperFileNameExtWhitelistTest.java
b/components/camel-http-base/src/test/java/org/apache/camel/http/base/HttpHelperFileNameExtWhitelistTest.java
new file mode 100644
index 000000000000..b6ab8eec7eed
--- /dev/null
+++
b/components/camel-http-base/src/test/java/org/apache/camel/http/base/HttpHelperFileNameExtWhitelistTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.http.base;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for the shared {@code fileNameExtWhitelist} check used by the HTTP
bindings and the platform-http-vertx
+ * consumer (CAMEL-24652). The important property is that an extension is
matched as a whole comma-separated token and
+ * never as a substring of the whitelist.
+ */
+class HttpHelperFileNameExtWhitelistTest {
+
+ @Test
+ void nullWhitelistAcceptsEverything() {
+ assertTrue(HttpHelper.isFileNameExtWhitelisted(null, "evil.exe"));
+ }
+
+ @Test
+ void starWhitelistAcceptsEverything() {
+ assertTrue(HttpHelper.isFileNameExtWhitelisted("*", "evil.exe"));
+ }
+
+ @Test
+ void aNameWithoutAnExtensionIsAccepted() {
+ assertTrue(HttpHelper.isFileNameExtWhitelisted("txt", "noextension"));
+ }
+
+ @Test
+ void anExactExtensionIsAccepted() {
+ assertTrue(HttpHelper.isFileNameExtWhitelisted("txt", "notes.txt"));
+ }
+
+ @Test
+ void aSubstringOfTheWhitelistIsNotAccepted() {
+ // the bug this guards: "txt".contains("x") must not accept "evil.x"
+ assertFalse(HttpHelper.isFileNameExtWhitelisted("txt", "evil.x"));
+ assertFalse(HttpHelper.isFileNameExtWhitelisted("txt", "evil.t"));
+ assertFalse(HttpHelper.isFileNameExtWhitelisted("txt", "evil.tx"));
+ }
+
+ @Test
+ void commaSeparatedTokensAreMatchedExactlyAndTrimmed() {
+ assertTrue(HttpHelper.isFileNameExtWhitelisted("txt,pdf",
"report.pdf"));
+ assertTrue(HttpHelper.isFileNameExtWhitelisted("txt, pdf",
"report.pdf"));
+ assertFalse(HttpHelper.isFileNameExtWhitelisted("txt,pdf",
"report.doc"));
+ }
+
+ @Test
+ void theMatchIsCaseInsensitive() {
+ assertTrue(HttpHelper.isFileNameExtWhitelisted("TXT", "notes.txt"));
+ assertTrue(HttpHelper.isFileNameExtWhitelisted("txt", "NOTES.TXT"));
+ }
+
+ @Test
+ void aMultiDotNameIsMatchedOnItsFullExtensionChain() {
+ // FileUtil.onlyExt returns everything after the first dot, so the
whole chain must be whitelisted
+ assertFalse(HttpHelper.isFileNameExtWhitelisted("gz",
"archive.tar.gz"));
+ assertTrue(HttpHelper.isFileNameExtWhitelisted("tar.gz",
"archive.tar.gz"));
+ }
+
+ @Test
+ void aDoubleExtensionIsNotAcceptedOnItsTrailingExtension() {
+ // evil.php.jpg must not pass a "jpg" whitelist: its extension chain
is "php.jpg", not "jpg"
+ assertFalse(HttpHelper.isFileNameExtWhitelisted("jpg",
"evil.php.jpg"));
+ }
+}
diff --git
a/components/camel-http-common/src/main/java/org/apache/camel/http/common/DefaultHttpBinding.java
b/components/camel-http-common/src/main/java/org/apache/camel/http/common/DefaultHttpBinding.java
index 797f83c3bd05..63c4cb50170b 100644
---
a/components/camel-http-common/src/main/java/org/apache/camel/http/common/DefaultHttpBinding.java
+++
b/components/camel-http-common/src/main/java/org/apache/camel/http/common/DefaultHttpBinding.java
@@ -55,7 +55,6 @@ import org.apache.camel.support.ExchangeHelper;
import org.apache.camel.support.GZIPHelper;
import org.apache.camel.support.MessageHelper;
import org.apache.camel.support.ObjectHelper;
-import org.apache.camel.util.FileUtil;
import org.apache.camel.util.IOHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -335,17 +334,7 @@ public class DefaultHttpBinding implements HttpBinding {
fileName = fileName.replaceAll("[\n\r\t]", "_");
}
// is the file name accepted
- boolean accepted = true;
- if (fileNameExtWhitelist != null) {
- String ext = FileUtil.onlyExt(fileName);
- if (ext != null) {
- ext = ext.toLowerCase(Locale.US);
- fileNameExtWhitelist =
fileNameExtWhitelist.toLowerCase(Locale.US);
- if (!fileNameExtWhitelist.equals("*") &&
!fileNameExtWhitelist.contains(ext)) {
- accepted = false;
- }
- }
- }
+ boolean accepted = isFileNameAccepted(fileName);
if (accepted) {
AttachmentMessage am =
message.getExchange().getMessage(AttachmentMessage.class);
am.addAttachment(fileName, new DataHandler(new
CamelFileDataSource(fileObject, fileName)));
@@ -358,6 +347,24 @@ public class DefaultHttpBinding implements HttpBinding {
}
}
+ /**
+ * Whether an uploaded file is accepted according to the configured {@link
#getFileNameExtWhitelist()}.
+ * <p/>
+ * The file name extension is compared, case-insensitively, against each
comma-separated entry of the whitelist
+ * exactly and not as a substring: a whitelist of "txt" must not accept an
upload named "evil.x" just because
+ * "txt".contains("x"). A file is accepted when no whitelist is
configured, when the whitelist is "*", or when the
+ * file name has no extension.
+ *
+ * @param fileName the file name submitted by the client
+ * @return true if the file is accepted
+ */
+ protected boolean isFileNameAccepted(String fileName) {
+ // one shared implementation of the whitelist check lives in
camel-http-base's HttpHelper so the servlet and
+ // jetty bindings here and the camel-platform-http-vertx consumer
cannot drift apart again (CAMEL-24652).
+ // Fully qualified because this package has its own HttpHelper with
the same simple name.
+ return
org.apache.camel.http.base.HttpHelper.isFileNameExtWhitelisted(getFileNameExtWhitelist(),
fileName);
+ }
+
@Override
public void writeResponse(Exchange exchange, HttpServletResponse response)
throws IOException {
Message target = exchange.getMessage();
diff --git
a/components/camel-http-common/src/test/java/org/apache/camel/http/common/DefaultHttpBindingTest.java
b/components/camel-http-common/src/test/java/org/apache/camel/http/common/DefaultHttpBindingTest.java
index f535d5ea7cdd..898b860985f8 100644
---
a/components/camel-http-common/src/test/java/org/apache/camel/http/common/DefaultHttpBindingTest.java
+++
b/components/camel-http-common/src/test/java/org/apache/camel/http/common/DefaultHttpBindingTest.java
@@ -24,7 +24,9 @@ import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
public class DefaultHttpBindingTest extends CamelTestSupport {
@@ -69,4 +71,69 @@ public class DefaultHttpBindingTest extends CamelTestSupport
{
String value = binding.convertHeaderValueToString(exchange, l);
assertEquals(value, l.toString());
}
+
+ @Test
+ public void testFileNameAcceptedWithoutWhitelist() {
+ DefaultHttpBinding binding = new DefaultHttpBinding();
+
+ assertTrue(binding.isFileNameAccepted("report.pdf"));
+ }
+
+ @Test
+ public void testFileNameAcceptedWithoutExtension() {
+ DefaultHttpBinding binding = new DefaultHttpBinding();
+ binding.setFileNameExtWhitelist("txt");
+
+ assertTrue(binding.isFileNameAccepted("README"));
+ assertTrue(binding.isFileNameAccepted(null));
+ }
+
+ @Test
+ public void testFileNameAcceptedWithWildcardWhitelist() {
+ DefaultHttpBinding binding = new DefaultHttpBinding();
+ binding.setFileNameExtWhitelist("*");
+
+ assertTrue(binding.isFileNameAccepted("report.pdf"));
+ assertTrue(binding.isFileNameAccepted("script.sh"));
+ }
+
+ @Test
+ public void testFileNameAcceptedMatchesEachExtension() {
+ DefaultHttpBinding binding = new DefaultHttpBinding();
+ binding.setFileNameExtWhitelist("txt, pdf");
+
+ assertTrue(binding.isFileNameAccepted("notes.txt"));
+ assertTrue(binding.isFileNameAccepted("report.pdf"));
+ assertFalse(binding.isFileNameAccepted("image.png"));
+ }
+
+ @Test
+ public void testFileNameAcceptedIsCaseInsensitive() {
+ DefaultHttpBinding binding = new DefaultHttpBinding();
+ binding.setFileNameExtWhitelist("TXT,pdf");
+
+ assertTrue(binding.isFileNameAccepted("notes.txt"));
+ assertTrue(binding.isFileNameAccepted("REPORT.PDF"));
+ }
+
+ @Test
+ public void testFileNameAcceptedDoesNotMatchSubstring() {
+ DefaultHttpBinding binding = new DefaultHttpBinding();
+ binding.setFileNameExtWhitelist("txt");
+
+ // "txt".contains("x") must not let an upload named evil.x through
+ assertFalse(binding.isFileNameAccepted("evil.x"));
+
+ binding.setFileNameExtWhitelist("txtdoc");
+ assertFalse(binding.isFileNameAccepted("notes.txt"));
+ }
+
+ @Test
+ public void testFileNameAcceptedDoesNotChangeConfiguredWhitelist() {
+ DefaultHttpBinding binding = new DefaultHttpBinding();
+ binding.setFileNameExtWhitelist("TXT");
+
+ binding.isFileNameAccepted("notes.txt");
+ assertEquals("TXT", binding.getFileNameExtWhitelist());
+ }
}
diff --git
a/components/camel-jetty/src/main/java/org/apache/camel/component/jetty12/AttachmentHttpBinding.java
b/components/camel-jetty/src/main/java/org/apache/camel/component/jetty12/AttachmentHttpBinding.java
index 872a6615ff1d..15d0afc084f4 100644
---
a/components/camel-jetty/src/main/java/org/apache/camel/component/jetty12/AttachmentHttpBinding.java
+++
b/components/camel-jetty/src/main/java/org/apache/camel/component/jetty12/AttachmentHttpBinding.java
@@ -21,7 +21,6 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Enumeration;
-import java.util.Locale;
import java.util.Map;
import jakarta.activation.DataHandler;
@@ -38,7 +37,6 @@ import org.apache.camel.attachment.DefaultAttachmentMessage;
import org.apache.camel.component.jetty.MultiPartFilter;
import org.apache.camel.http.common.DefaultHttpBinding;
import org.apache.camel.http.common.HttpHelper;
-import org.apache.camel.util.FileUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -101,30 +99,6 @@ final class AttachmentHttpBinding extends
DefaultHttpBinding {
}
}
- private boolean isFileNameAccepted(String fileName) {
- String whitelist = getFileNameExtWhitelist();
- if (whitelist == null) {
- return true;
- }
- String ext = FileUtil.onlyExt(fileName);
- if (ext == null) {
- return true;
- }
- ext = ext.toLowerCase(Locale.US);
- whitelist = whitelist.toLowerCase(Locale.US);
- if (whitelist.equals("*")) {
- return true;
- }
- // compare against each comma-separated extension exactly, not as a
substring: a whitelist of "txt"
- // must not accept an upload named "evil.x" just because
"txt".contains("x")
- for (String allowed : whitelist.split(",")) {
- if (allowed.trim().equals(ext)) {
- return true;
- }
- }
- return false;
- }
-
@Override
protected void populateRequestParameters(HttpServletRequest request,
Message message) {
// we populate the http request parameters without checking the request
diff --git a/components/camel-platform-http-vertx/pom.xml
b/components/camel-platform-http-vertx/pom.xml
index 68772f17063c..5acd0c5f1d13 100644
--- a/components/camel-platform-http-vertx/pom.xml
+++ b/components/camel-platform-http-vertx/pom.xml
@@ -42,6 +42,10 @@
<groupId>org.apache.camel</groupId>
<artifactId>camel-platform-http</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-http-base</artifactId>
+ </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-attachments</artifactId>
diff --git
a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
index d5a248039cd4..e48193f9c779 100644
---
a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
+++
b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
@@ -53,11 +53,11 @@ import
org.apache.camel.component.platform.http.cookie.CookieConfiguration;
import org.apache.camel.component.platform.http.cookie.CookieHandler;
import org.apache.camel.component.platform.http.spi.Method;
import org.apache.camel.component.platform.http.spi.PlatformHttpConsumer;
+import org.apache.camel.http.base.HttpHelper;
import org.apache.camel.spi.HeaderFilterStrategy;
import org.apache.camel.spi.RestRegistry;
import org.apache.camel.support.DefaultConsumer;
import org.apache.camel.support.PluginHelper;
-import org.apache.camel.util.FileUtil;
import org.apache.camel.util.MimeTypeHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -449,18 +449,10 @@ public class VertxPlatformHttpConsumer extends
DefaultConsumer
LOGGER.trace("HTTP attachment {} = {}", name, fileName);
- // is the file name accepted
- boolean accepted = true;
-
- if (fileNameExtWhitelist != null) {
- String ext = FileUtil.onlyExt(fileName);
- if (ext != null) {
- ext = ext.toLowerCase(Locale.US);
- if (!fileNameExtWhitelist.equals("*") &&
!fileNameExtWhitelist.contains(ext)) {
- accepted = false;
- }
- }
- }
+ // is the file name accepted - shared with the http bindings so
the whitelist matches whole
+ // comma-separated extension tokens, not a substring: a whitelist
of "txt" must not accept an upload
+ // named "evil.x" just because "txt".contains("x") (CAMEL-24652)
+ boolean accepted =
HttpHelper.isFileNameExtWhitelisted(fileNameExtWhitelist, fileName);
if (accepted) {
final File localFile = new File(upload.uploadedFileName());
final AttachmentMessage attachmentMessage =
message.getExchange().getMessage(AttachmentMessage.class);
diff --git
a/components/camel-servlet/src/main/java/org/apache/camel/component/servlet/AttachmentHttpBinding.java
b/components/camel-servlet/src/main/java/org/apache/camel/component/servlet/AttachmentHttpBinding.java
index aeb78820d1b2..e60c8f94219f 100644
---
a/components/camel-servlet/src/main/java/org/apache/camel/component/servlet/AttachmentHttpBinding.java
+++
b/components/camel-servlet/src/main/java/org/apache/camel/component/servlet/AttachmentHttpBinding.java
@@ -20,7 +20,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
-import java.util.Locale;
import jakarta.activation.DataSource;
import jakarta.servlet.http.HttpServletRequest;
@@ -34,7 +33,6 @@ import org.apache.camel.attachment.DefaultAttachment;
import org.apache.camel.attachment.DefaultAttachmentMessage;
import org.apache.camel.http.common.DefaultHttpBinding;
import org.apache.camel.http.common.HttpHelper;
-import org.apache.camel.util.FileUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -59,15 +57,7 @@ public final class AttachmentHttpBinding extends
DefaultHttpBinding {
// name and not against Part.getName(), which is the multipart
field name
String fileName = part.getSubmittedFileName();
// is the file name accepted
- boolean accepted = true;
- if (getFileNameExtWhitelist() != null) {
- String ext = FileUtil.onlyExt(fileName);
- if (ext != null) {
- ext = ext.toLowerCase(Locale.US);
- String whiteList =
getFileNameExtWhitelist().toLowerCase(Locale.US);
- accepted = whiteList.equals("*") ||
isExtWhitelisted(whiteList, ext);
- }
- }
+ boolean accepted = isFileNameAccepted(fileName);
if (accepted) {
DataSource ds = new PartDataSource(part);
@@ -90,17 +80,6 @@ public final class AttachmentHttpBinding extends
DefaultHttpBinding {
}
}
- // compare against each comma-separated extension exactly, not as a
substring: a whitelist of "txt"
- // must not accept an upload named "evil.x" just because
"txt".contains("x")
- private static boolean isExtWhitelisted(String whitelist, String ext) {
- for (String allowed : whitelist.split(",")) {
- if (allowed.trim().equals(ext)) {
- return true;
- }
- }
- return false;
- }
-
public final class PartDataSource implements DataSource {
private final Part part;