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);
