From bf7e83efe35115780f66088267172ce1a8105f74 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Thu, 12 Mar 2026 12:05:52 +0000 Subject: [PATCH] Reject path traversal in debian and rpm handlers The debian and rpm handlers take the request path and pass it directly to the upstream URL without checking for ".." segments. This could let a client craft a request that reaches unintended upstream paths. Add a containsPathTraversal check at the entry point of both handlers and return 400 for any path containing ".." segments. --- internal/handler/debian.go | 5 +++++ internal/handler/debian_test.go | 9 +++++++++ internal/handler/handler.go | 12 +++++++++++ internal/handler/path_traversal_test.go | 27 +++++++++++++++++++++++++ internal/handler/rpm.go | 5 +++++ internal/handler/rpm_test.go | 9 +++++++++ 6 files changed, 67 insertions(+) create mode 100644 internal/handler/path_traversal_test.go diff --git a/internal/handler/debian.go b/internal/handler/debian.go index e413ca3..1fc7c36 100644 --- a/internal/handler/debian.go +++ b/internal/handler/debian.go @@ -40,6 +40,11 @@ func (h *DebianHandler) Routes() http.Handler { path := strings.TrimPrefix(r.URL.Path, "/") + if containsPathTraversal(path) { + http.Error(w, "invalid path", http.StatusBadRequest) + return + } + // Route based on path type switch { case strings.HasPrefix(path, "pool/"): diff --git a/internal/handler/debian_test.go b/internal/handler/debian_test.go index bebffcf..77d6843 100644 --- a/internal/handler/debian_test.go +++ b/internal/handler/debian_test.go @@ -86,4 +86,13 @@ func TestDebianHandler_Routes(t *testing.T) { if w.Code != http.StatusMethodNotAllowed { t.Errorf("POST request: got status %d, want %d", w.Code, http.StatusMethodNotAllowed) } + + // Test path traversal rejection + req = httptest.NewRequest(http.MethodGet, "/pool/../../../etc/passwd", nil) + w = httptest.NewRecorder() + handler.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("path traversal: got status %d, want %d", w.Code, http.StatusBadRequest) + } } diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 9e906b9..9cf6e2d 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -8,6 +8,7 @@ import ( "io" "log/slog" "net/http" + "strings" "time" "github.com/git-pkgs/proxy/internal/cooldown" @@ -18,6 +19,17 @@ import ( "github.com/git-pkgs/registries/fetch" ) +// containsPathTraversal returns true if the path contains ".." segments +// that could be used to escape the intended directory. +func containsPathTraversal(path string) bool { + for _, segment := range strings.Split(path, "/") { + if segment == ".." { + return true + } + } + return false +} + // Proxy provides shared functionality for protocol handlers. type Proxy struct { DB *database.DB diff --git a/internal/handler/path_traversal_test.go b/internal/handler/path_traversal_test.go new file mode 100644 index 0000000..14d2218 --- /dev/null +++ b/internal/handler/path_traversal_test.go @@ -0,0 +1,27 @@ +package handler + +import "testing" + +func TestContainsPathTraversal(t *testing.T) { + tests := []struct { + path string + want bool + }{ + {"pool/main/n/nginx/nginx_1.0.deb", false}, + {"releases/39/Packages/test.rpm", false}, + {"../etc/passwd", true}, + {"pool/../../etc/passwd", true}, + {"pool/main/../../../etc/shadow", true}, + {"pool/..hidden/file", false}, // ".." as a segment, not "..hidden" + {"", false}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + got := containsPathTraversal(tt.path) + if got != tt.want { + t.Errorf("containsPathTraversal(%q) = %v, want %v", tt.path, got, tt.want) + } + }) + } +} diff --git a/internal/handler/rpm.go b/internal/handler/rpm.go index cff5b00..12ce9dc 100644 --- a/internal/handler/rpm.go +++ b/internal/handler/rpm.go @@ -41,6 +41,11 @@ func (h *RPMHandler) Routes() http.Handler { path := strings.TrimPrefix(r.URL.Path, "/") + if containsPathTraversal(path) { + http.Error(w, "invalid path", http.StatusBadRequest) + return + } + // Route based on path type switch { case strings.HasSuffix(path, ".rpm"): diff --git a/internal/handler/rpm_test.go b/internal/handler/rpm_test.go index b24d8fa..b63f44d 100644 --- a/internal/handler/rpm_test.go +++ b/internal/handler/rpm_test.go @@ -86,4 +86,13 @@ func TestRPMHandler_Routes(t *testing.T) { if w.Code != http.StatusMethodNotAllowed { t.Errorf("POST request: got status %d, want %d", w.Code, http.StatusMethodNotAllowed) } + + // Test path traversal rejection + req = httptest.NewRequest(http.MethodGet, "/releases/../../../etc/passwd", nil) + w = httptest.NewRecorder() + handler.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("path traversal: got status %d, want %d", w.Code, http.StatusBadRequest) + } }