Skip to content

Commit bf7e83e

Browse files
committed
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.
1 parent 9b321ea commit bf7e83e

6 files changed

Lines changed: 67 additions & 0 deletions

File tree

internal/handler/debian.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ func (h *DebianHandler) Routes() http.Handler {
4040

4141
path := strings.TrimPrefix(r.URL.Path, "/")
4242

43+
if containsPathTraversal(path) {
44+
http.Error(w, "invalid path", http.StatusBadRequest)
45+
return
46+
}
47+
4348
// Route based on path type
4449
switch {
4550
case strings.HasPrefix(path, "pool/"):

internal/handler/debian_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,13 @@ func TestDebianHandler_Routes(t *testing.T) {
8686
if w.Code != http.StatusMethodNotAllowed {
8787
t.Errorf("POST request: got status %d, want %d", w.Code, http.StatusMethodNotAllowed)
8888
}
89+
90+
// Test path traversal rejection
91+
req = httptest.NewRequest(http.MethodGet, "/pool/../../../etc/passwd", nil)
92+
w = httptest.NewRecorder()
93+
handler.ServeHTTP(w, req)
94+
95+
if w.Code != http.StatusBadRequest {
96+
t.Errorf("path traversal: got status %d, want %d", w.Code, http.StatusBadRequest)
97+
}
8998
}

internal/handler/handler.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"log/slog"
1010
"net/http"
11+
"strings"
1112
"time"
1213

1314
"github.com/git-pkgs/proxy/internal/cooldown"
@@ -18,6 +19,17 @@ import (
1819
"github.com/git-pkgs/registries/fetch"
1920
)
2021

22+
// containsPathTraversal returns true if the path contains ".." segments
23+
// that could be used to escape the intended directory.
24+
func containsPathTraversal(path string) bool {
25+
for _, segment := range strings.Split(path, "/") {
26+
if segment == ".." {
27+
return true
28+
}
29+
}
30+
return false
31+
}
32+
2133
// Proxy provides shared functionality for protocol handlers.
2234
type Proxy struct {
2335
DB *database.DB
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package handler
2+
3+
import "testing"
4+
5+
func TestContainsPathTraversal(t *testing.T) {
6+
tests := []struct {
7+
path string
8+
want bool
9+
}{
10+
{"pool/main/n/nginx/nginx_1.0.deb", false},
11+
{"releases/39/Packages/test.rpm", false},
12+
{"../etc/passwd", true},
13+
{"pool/../../etc/passwd", true},
14+
{"pool/main/../../../etc/shadow", true},
15+
{"pool/..hidden/file", false}, // ".." as a segment, not "..hidden"
16+
{"", false},
17+
}
18+
19+
for _, tt := range tests {
20+
t.Run(tt.path, func(t *testing.T) {
21+
got := containsPathTraversal(tt.path)
22+
if got != tt.want {
23+
t.Errorf("containsPathTraversal(%q) = %v, want %v", tt.path, got, tt.want)
24+
}
25+
})
26+
}
27+
}

internal/handler/rpm.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ func (h *RPMHandler) Routes() http.Handler {
4141

4242
path := strings.TrimPrefix(r.URL.Path, "/")
4343

44+
if containsPathTraversal(path) {
45+
http.Error(w, "invalid path", http.StatusBadRequest)
46+
return
47+
}
48+
4449
// Route based on path type
4550
switch {
4651
case strings.HasSuffix(path, ".rpm"):

internal/handler/rpm_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,13 @@ func TestRPMHandler_Routes(t *testing.T) {
8686
if w.Code != http.StatusMethodNotAllowed {
8787
t.Errorf("POST request: got status %d, want %d", w.Code, http.StatusMethodNotAllowed)
8888
}
89+
90+
// Test path traversal rejection
91+
req = httptest.NewRequest(http.MethodGet, "/releases/../../../etc/passwd", nil)
92+
w = httptest.NewRecorder()
93+
handler.ServeHTTP(w, req)
94+
95+
if w.Code != http.StatusBadRequest {
96+
t.Errorf("path traversal: got status %d, want %d", w.Code, http.StatusBadRequest)
97+
}
8998
}

0 commit comments

Comments
 (0)