From c9ae9f99cb908082027abab479b4d992c1a69e8e Mon Sep 17 00:00:00 2001 From: Paolo Valletta Date: Mon, 9 Nov 2026 16:08:13 +0100 Subject: [PATCH 1/3] Answer 404 for Maven paths too short to hold a coordinate handleDownload only 404'd an unparseable path when it had four or more segments, so a file such as bar/1.0/bar-1.0.jar was proxied without caching and skipped the denylist, the scan and the cooldown. Nothing in the Maven layout is shorter than a coordinate, so any path it cannot parse now gets the same 404, and the test for paths outside the layout covers the short ones. Closes #686. --- internal/handler/maven.go | 13 +++++-------- internal/handler/maven_repositories_test.go | 10 ++++++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/internal/handler/maven.go b/internal/handler/maven.go index e841d083..cd947633 100644 --- a/internal/handler/maven.go +++ b/internal/handler/maven.go @@ -221,14 +221,11 @@ func (h *MavenHandler) handleDownload(w http.ResponseWriter, r *http.Request, so // e.g., com/google/guava/guava/32.1.3-jre/guava-32.1.3-jre.jar group, artifact, version, filename := h.parsePath(urlPath) if artifact == "" { - if strings.Count(urlPath, "/") >= minMavenParts-1 { - // A dot or colon in a group directory, or a colon in the - // artifact, which the Maven layout never has. Proxied, the - // file would skip the denylist, scan and cooldown. - http.NotFound(w, r) - return - } - h.proxyUpstream(w, r, source, urlPath) + // Too few segments to hold a coordinate, or a dot or colon in a + // group directory, or a colon in the artifact, which the Maven + // layout never has. Proxied, the file would skip the denylist, + // scan and cooldown. + http.NotFound(w, r) return } diff --git a/internal/handler/maven_repositories_test.go b/internal/handler/maven_repositories_test.go index e9b2fcf4..5c9dd717 100644 --- a/internal/handler/maven_repositories_test.go +++ b/internal/handler/maven_repositories_test.go @@ -139,15 +139,17 @@ func TestMavenRepositoryRepointedServesTheNewUpstream(t *testing.T) { } } -// TestMavenDottedGroupDirectoryNotCachedAsGroup checks that a file whose -// group directories contain dots or colons, which the Maven layout never -// has, is not served: it would either share the cache entry of the group -// the dots spell or be proxied past the denylist, scan and cooldown. +// TestMavenDottedGroupDirectoryNotCachedAsGroup checks that a file outside +// the Maven layout is not served: a group directory with a dot or a colon, +// which the layout never has and which would share the cache entry of the +// group the dots spell, and a path too short to hold a coordinate. Proxied, +// they would skip the denylist, scan and cooldown. func TestMavenDottedGroupDirectoryNotCachedAsGroup(t *testing.T) { const standard = "/org/foo/bar/1.0/bar-1.0.jar" odd := []string{ "/org.foo/bar/1.0/bar-1.0.jar", "/org.foo/bar/1.0/bar-1.0-linux.exe", "/org/foo:x/bar/1.0/bar-1.0.jar", "/org/foo/b:ar/1.0/b:ar-1.0.jar", + "/bar/1.0/bar-1.0.jar", "/bar-1.0.jar", } central, requests := mavenRepoUpstream(t, "central", append([]string{standard}, odd...)...) portal, _ := mavenRepoUpstream(t, "portal") From b08dc1aa1931d2622a4e9fb0191ba305d1037763 Mon Sep 17 00:00:00 2001 From: Simon Chrzanowski Date: Fri, 13 Nov 2026 07:00:23 +0100 Subject: [PATCH 2/3] Keep proxying short checksums of files that aren't artifacts Answering 404 for every path too short to parse also turned checksums such as /archetype-catalog.xml.sha1 and /.index/nexus-maven-repository-index.gz.sha1 into 404s, which Maven Central serves. A short path now gets 404 only when it is an artifact, a checksum or signature of one, or a coordinate file. --- internal/handler/download_test.go | 4 ++++ internal/handler/maven.go | 23 +++++++++++++++------ internal/handler/maven_repositories_test.go | 2 +- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/internal/handler/download_test.go b/internal/handler/download_test.go index c53585e2..d0582366 100644 --- a/internal/handler/download_test.go +++ b/internal/handler/download_test.go @@ -960,6 +960,10 @@ func TestMavenHandler_NonArtifactPathsProxied(t *testing.T) { "/com/google/protobuf/protoc/3.21.0/", "/com/google/protobuf/protoc/", "/archetype-catalog.xml", + // Checksums of files that aren't artifacts, too short to parse. + "/archetype-catalog.xml.sha1", + "/archetype-catalog.xml.md5", + "/.index/nexus-maven-repository-index.gz.sha1", } for _, path := range paths { w := httptest.NewRecorder() diff --git a/internal/handler/maven.go b/internal/handler/maven.go index bded85a5..a8f9b9ab 100644 --- a/internal/handler/maven.go +++ b/internal/handler/maven.go @@ -243,9 +243,14 @@ func (h *MavenHandler) handleDownload(w http.ResponseWriter, r *http.Request, so if artifact == "" { // Too few segments to hold a coordinate, or a dot or colon in a // group directory, or a colon in the artifact, which the Maven - // layout never has. Proxied, the file would skip the denylist, - // scan and cooldown. - http.NotFound(w, r) + // layout never has. Proxied, an artifact would skip the denylist, + // scan and cooldown. A checksum of another file, such as + // archetype-catalog.xml.sha1, is proxied like that file. + if h.isArtifactFile(h.checksummedFile(path.Base(urlPath))) || h.isCoordinateFile(urlPath) { + http.NotFound(w, r) + return + } + h.proxyUpstream(w, r, source, urlPath) return } @@ -381,12 +386,18 @@ func (h *MavenHandler) isVersionListingFile(filename string) bool { // an artifact. They are as immutable as the artifact, so they are cached // with it rather than refetched on every request. func (h *MavenHandler) isChecksumFile(filename string) bool { + return h.checksummedFile(filename) != filename +} + +// checksummedFile is the file a checksum or signature file is for +// (bar-1.0.jar for bar-1.0.jar.sha1), or filename itself if it is neither. +func (h *MavenHandler) checksummedFile(filename string) string { for _, ext := range []string{".sha1", ".sha256", ".sha512", ".md5", ".asc"} { - if strings.HasSuffix(filename, ext) { - return true + if name, ok := strings.CutSuffix(filename, ext); ok { + return name } } - return false + return filename } // mavenForwardHeaders lets clients resume artifacts proxied without caching. diff --git a/internal/handler/maven_repositories_test.go b/internal/handler/maven_repositories_test.go index 5c9dd717..4e92861e 100644 --- a/internal/handler/maven_repositories_test.go +++ b/internal/handler/maven_repositories_test.go @@ -149,7 +149,7 @@ func TestMavenDottedGroupDirectoryNotCachedAsGroup(t *testing.T) { odd := []string{ "/org.foo/bar/1.0/bar-1.0.jar", "/org.foo/bar/1.0/bar-1.0-linux.exe", "/org/foo:x/bar/1.0/bar-1.0.jar", "/org/foo/b:ar/1.0/b:ar-1.0.jar", - "/bar/1.0/bar-1.0.jar", "/bar-1.0.jar", + "/bar/1.0/bar-1.0.jar", "/bar-1.0.jar", "/bar/1.0/bar-1.0.jar.sha1", "/bar-1.0.pom.asc", } central, requests := mavenRepoUpstream(t, "central", append([]string{standard}, odd...)...) portal, _ := mavenRepoUpstream(t, "portal") From 2ddbd6266fb206ae6ac23772a2fa7d955ecddcd9 Mon Sep 17 00:00:00 2001 From: Simon Chrzanowski Date: Fri, 13 Nov 2026 07:12:17 +0100 Subject: [PATCH 3/3] Answer 404 for short checksums of signatures too checksummedFile cut one suffix, so /bar/1.0/bar-1.0.jar.asc.sha1 named bar-1.0.jar.asc, which isn't an artifact, and was proxied. It now cuts every checksum and signature suffix. --- internal/handler/maven.go | 18 +++++++++++++----- internal/handler/maven_repositories_test.go | 1 + 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/internal/handler/maven.go b/internal/handler/maven.go index a8f9b9ab..39af4407 100644 --- a/internal/handler/maven.go +++ b/internal/handler/maven.go @@ -390,14 +390,22 @@ func (h *MavenHandler) isChecksumFile(filename string) bool { } // checksummedFile is the file a checksum or signature file is for -// (bar-1.0.jar for bar-1.0.jar.sha1), or filename itself if it is neither. +// (bar-1.0.jar for bar-1.0.jar.sha1 and bar-1.0.jar.asc.sha1), or filename +// itself if it is neither. func (h *MavenHandler) checksummedFile(filename string) string { - for _, ext := range []string{".sha1", ".sha256", ".sha512", ".md5", ".asc"} { - if name, ok := strings.CutSuffix(filename, ext); ok { - return name + for { + trimmed := filename + for _, ext := range []string{".sha1", ".sha256", ".sha512", ".md5", ".asc"} { + if name, ok := strings.CutSuffix(trimmed, ext); ok { + trimmed = name + break + } } + if trimmed == filename { + return filename + } + filename = trimmed } - return filename } // mavenForwardHeaders lets clients resume artifacts proxied without caching. diff --git a/internal/handler/maven_repositories_test.go b/internal/handler/maven_repositories_test.go index 4e92861e..4249847d 100644 --- a/internal/handler/maven_repositories_test.go +++ b/internal/handler/maven_repositories_test.go @@ -150,6 +150,7 @@ func TestMavenDottedGroupDirectoryNotCachedAsGroup(t *testing.T) { "/org.foo/bar/1.0/bar-1.0.jar", "/org.foo/bar/1.0/bar-1.0-linux.exe", "/org/foo:x/bar/1.0/bar-1.0.jar", "/org/foo/b:ar/1.0/b:ar-1.0.jar", "/bar/1.0/bar-1.0.jar", "/bar-1.0.jar", "/bar/1.0/bar-1.0.jar.sha1", "/bar-1.0.pom.asc", + "/bar/1.0/bar-1.0.jar.asc.sha1", } central, requests := mavenRepoUpstream(t, "central", append([]string{standard}, odd...)...) portal, _ := mavenRepoUpstream(t, "portal")