This is an automated email from the ASF dual-hosted git repository.

luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-kit.git

commit 011ff80f45d862b1a5e6b78c0888c4db99c4063d
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Oct 30 10:29:21 2026 +0100

    Fix #137: require https (or loopback http) for remote knowledge index URIs
    
    The index manifest carries the sha256 that verifies the archive, so a 
manifest
    fetched over cleartext http can be rewritten in transit together with its 
hash,
    defeating the integrity check. Add requireFetchableUri(): allow https and 
local
    file URIs anywhere, and http only to a loopback host (keeps local testing 
and the
    loopback test fixtures working). Apply it to the configured manifest URL, 
the
    release discovery endpoint, the discovered release-asset manifest URL, and 
the
    resolved asset URL.
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 .../camel/kit/knowledge/mcp/IndexDownloader.java   | 41 +++++++++++++++++++---
 .../kit/knowledge/mcp/IndexDownloaderTest.java     | 18 ++++++++++
 2 files changed, 55 insertions(+), 4 deletions(-)

diff --git 
a/knowledge/mcp/src/main/java/org/apache/camel/kit/knowledge/mcp/IndexDownloader.java
 
b/knowledge/mcp/src/main/java/org/apache/camel/kit/knowledge/mcp/IndexDownloader.java
index 16128063..ce7c1723 100644
--- 
a/knowledge/mcp/src/main/java/org/apache/camel/kit/knowledge/mcp/IndexDownloader.java
+++ 
b/knowledge/mcp/src/main/java/org/apache/camel/kit/knowledge/mcp/IndexDownloader.java
@@ -268,9 +268,12 @@ public class IndexDownloader {
 
     private ManifestLocation discoverManifest() throws IOException, 
InterruptedException {
         if (config.url().isPresent()) {
-            return new ManifestLocation(URI.create(config.url().get()), 
config.version().orElse(null));
+            URI manifestUri = URI.create(config.url().get());
+            requireFetchableUri(manifestUri);
+            return new ManifestLocation(manifestUri, 
config.version().orElse(null));
         }
         URI endpoint = URI.create(config.releasesUrl());
+        requireFetchableUri(endpoint);
         if (config.version().isPresent()) {
             URI pinned = URI.create(endpoint.toString().replaceAll("/+$", "")
                                     + "/tags/" + RELEASE_PREFIX + 
config.version().get());
@@ -330,9 +333,7 @@ public class IndexDownloader {
             if ("index.json".equals(asset.path("name").asText())) {
                 try {
                     URI uri = 
URI.create(asset.path("browser_download_url").asText());
-                    if (!Set.of("https", "http", 
"file").contains(uri.getScheme())) {
-                        throw new IOException("Unsupported index manifest URI: 
" + uri);
-                    }
+                    requireFetchableUri(uri);
                     return Optional.of(uri);
                 } catch (IllegalArgumentException e) {
                     throw new IOException("Invalid index manifest download 
URL", e);
@@ -372,6 +373,37 @@ public class IndexDownloader {
         }
     }
 
+    /**
+     * The manifest carries the sha256 that verifies the archive, so fetching 
the manifest (or the asset) over cleartext
+     * would let a man-in-the-middle rewrite both the content and its expected 
hash. Allow {@code https} and local
+     * {@code file} URIs anywhere; allow {@code http} only to a loopback host, 
which has no network attacker and keeps
+     * local testing working.
+     */
+    private static void requireFetchableUri(URI uri) throws IOException {
+        String scheme = uri.getScheme() == null ? "" : 
uri.getScheme().toLowerCase(Locale.ROOT);
+        switch (scheme) {
+            case "https", "file" -> {
+            }
+            case "http" -> {
+                if (!isLoopbackHost(uri.getHost())) {
+                    throw new IOException(
+                            "Refusing to fetch the knowledge index over 
cleartext http from a non-loopback host; "
+                                          + "use https, since the manifest's 
sha256 cannot protect a manifest served over http: "
+                                          + uri);
+                }
+            }
+            default -> throw new IOException("Unsupported index URI scheme 
(expected https or file): " + uri);
+        }
+    }
+
+    private static boolean isLoopbackHost(String host) {
+        if (host == null) {
+            return false;
+        }
+        String h = host.toLowerCase(Locale.ROOT);
+        return h.equals("localhost") || h.equals("::1") || h.equals("[::1]") 
|| h.matches("127(\\.\\d{1,3}){3}");
+    }
+
     private static Path versionDirectory(Path cacheDir, String version) throws 
IOException {
         validateVersion(version);
         Path dir = cacheDir.resolve(version);
@@ -506,6 +538,7 @@ public class IndexDownloader {
         } catch (IllegalArgumentException e) {
             throw new IOException("Invalid index asset URI: " + 
manifest.asset(), e);
         }
+        requireFetchableUri(assetUri);
         LOG.info("Downloading knowledge index {} from {} ...", 
manifest.version(), assetUri);
 
         Path zipFile = Files.createTempFile(cacheDir, "index-", ".zip.part");
diff --git 
a/knowledge/mcp/src/test/java/org/apache/camel/kit/knowledge/mcp/IndexDownloaderTest.java
 
b/knowledge/mcp/src/test/java/org/apache/camel/kit/knowledge/mcp/IndexDownloaderTest.java
index 214c9179..64d189a2 100644
--- 
a/knowledge/mcp/src/test/java/org/apache/camel/kit/knowledge/mcp/IndexDownloaderTest.java
+++ 
b/knowledge/mcp/src/test/java/org/apache/camel/kit/knowledge/mcp/IndexDownloaderTest.java
@@ -187,6 +187,24 @@ class IndexDownloaderTest {
                 "Error must point the user at the escape hatches: " + 
e.getMessage());
     }
 
+    @Test
+    void nonLoopbackHttpManifestIsRejected() throws Exception {
+        publishDir = tmp.resolve("publish");
+        Files.createDirectories(publishDir);
+        cacheDir = tmp.resolve("cache");
+
+        IOException e = assertThrows(IOException.class,
+                () -> downloader(null, 
"http://example.org/index.json";).resolve());
+        boolean refused = false;
+        for (Throwable t = e; t != null; t = t.getCause()) {
+            if (t.getMessage() != null && t.getMessage().contains("cleartext 
http")) {
+                refused = true;
+                break;
+            }
+        }
+        assertTrue(refused, "A remote http manifest must be refused over 
cleartext: " + e);
+    }
+
     @Test
     void newVersionReplacesOldAndPrunes() throws Exception {
         String url = publish("2026.07.01", "old-bytes", null);

Reply via email to