Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/server/dot_segments_test.go
Simon Chrzanowski 1fbee102b1 Ignore path parameters when checking for dot segments
Tomcat and Jetty drop ";params" from a path segment before resolving it,
so "..;" passed the dot-segment check and still got the credentials
configured for the upstream path. The check now ignores ";params", and
package routes refuse ';' like '?' and '#', which also stops "1.0;x"
from passing the denylist as a different version. Composer archives
stay exempt, since git branch names may contain ';'.
2026-10-23 09:47:07 +02:00

97 lines
3.3 KiB
Go

package server
import (
"context"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"github.com/git-pkgs/proxy/internal/config"
"github.com/git-pkgs/proxy/internal/database"
"github.com/git-pkgs/proxy/internal/handler"
"github.com/git-pkgs/proxy/internal/storage"
"github.com/git-pkgs/registries/fetch"
"github.com/go-chi/chi/v5"
)
// TestProtocolHandlersRejectDotSegments sends paths with encoded '.' and '..'
// segments, and with ';' path parameters, to every mounted ecosystem. Several
// handlers append the decoded path to the upstream URL, where a '..' leaves
// the configured upstream path, and Tomcat and Jetty drop ";params" first.
func TestProtocolHandlersRejectDotSegments(t *testing.T) {
db, err := database.Create(t.TempDir() + "/test.db")
if err != nil {
t.Fatalf("creating database: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
store, err := storage.OpenBucket(context.Background(), "file://"+t.TempDir())
if err != nil {
t.Fatalf("opening storage: %v", err)
}
transport := &countingTransport{}
client := &http.Client{Transport: transport}
fetcher := fetch.NewFetcher(fetch.WithHTTPClient(client), fetch.WithMaxRetries(0))
t.Cleanup(func() { _ = fetcher.Close() })
proxy := handler.NewProxy(db, store, fetcher, fetch.NewResolver(), slog.New(slog.NewTextHandler(io.Discard, nil)))
proxy.HTTPClient = client
cfg := config.Default()
cfg.BaseURL = "http://proxy.example"
s := &Server{cfg: cfg}
r := chi.NewRouter()
s.mountProtocolHandlers(r, proxy)
for _, target := range []string{
"/maven/%2e%2e/%2e%2e/secret",
"/maven/com/%2e%2e/%2e%2e/x/maven-metadata.xml",
"/maven/com/%2E%2E/x/1.0/x-1.0.jar",
"/maven/com/%2e/x/1.0/x-1.0.jar",
"/maven/com/%252e%252e/x/maven-metadata.xml",
"/conda/c/%2e%2e/x.json",
"/cran/src/contrib/%2e%2e%2fx.tar.gz",
"/cran/src/contrib/..%5cx.tar.gz",
"/go/a/%2e%2e/%2e%2e/x/@v/list",
"/gem/info/%2e%2e%2f%2e%2e%2fx",
"/gem/gems/%2e%2e%2fx-1.0.gem",
"/nuget/v3-flatcontainer/%2e%2e/x/index.json",
"/debian/pool/%2e%2e/x.deb",
"/rpm/%2e%2e/x.rpm",
"/maven/..;/..;/other-repo/com/x/x/1.0/x-1.0.jar",
"/maven/com/%2e%2e;/x/1.0/x-1.0.jar",
"/maven/com/..%3b/x/1.0/x-1.0.jar",
"/maven/com/evil/evil/1.0;x/evil-1.0.jar",
"/generic/github/o/r/releases/download/v1/tool.tar.gz;x",
} {
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, target, nil))
if w.Code != http.StatusBadRequest {
t.Errorf("%s: status = %d, want 400", target, w.Code)
}
}
if n := transport.hits.Load(); n != 0 {
t.Errorf("upstream was contacted %d times", n)
}
}
// TestDotSegmentGuardKeepsDottedNames keeps names that only contain dots,
// such as versions and file extensions, working.
func TestDotSegmentGuardKeepsDottedNames(t *testing.T) {
for _, target := range []string{
"/maven/com/example/x/1.0/x-1.0.jar",
"/pypi/simple/zope.interface/",
"/npm/.bin",
"/generic/github/o/r/releases/download/v1/...tar.gz",
"/maven/com/example/x/1..0/x-1..0.jar",
} {
var reached bool
next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { reached = true })
w := httptest.NewRecorder()
rejectDotSegments(next).ServeHTTP(w, httptest.NewRequest(http.MethodGet, target, nil))
if !reached {
t.Errorf("%s: status = %d, want the request passed on", target, w.Code)
}
}
}