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) + } }