bidet

commit 492a05b8e658a3f2626210f5e33dc874141545f7

tree

parent:
c362fc5ff73740244ceaacba31cdae3e6098ac04

nmyk <nick@nmyk.io>

2026-04-21T20:28:21-04:00

fix path traversal vuln

diff --git a/go.mod b/go.mod
index c500eda0bc79434ce2d33a0d2046cd7bb9d818f9..c4a1cd2a7810e8bc0bae0fba4c61a6c547fba3f4 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@
 go 1.25.0
 
 require (
+	github.com/cyphar/filepath-securejoin v0.6.1
 	github.com/dustin/go-humanize v1.0.1
 	github.com/go-git/go-git/v6 v6.0.0-20260220113129-c02711164eb8
 	github.com/peterbourgon/ff/v4 v4.0.0-beta.1
@@ -12,7 +13,6 @@ require (
 	github.com/Microsoft/go-winio v0.6.2 // indirect
 	github.com/ProtonMail/go-crypto v1.3.0 // indirect
 	github.com/cloudflare/circl v1.6.1 // indirect
-	github.com/cyphar/filepath-securejoin v0.6.1 // indirect
 	github.com/emirpasic/gods v1.18.1 // indirect
 	github.com/go-git/gcfg/v2 v2.0.2 // indirect
 	github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc // indirect
diff --git a/internal/handlers/directory.go b/internal/handlers/directory.go
index 6043457d4d2bdd4bf1971b67e21ea5ac65e93637..8ef0dd8bd5925d218e2ce5e25f8b7e016f080103 100644
--- a/internal/handlers/directory.go
+++ b/internal/handlers/directory.go
@@ -3,9 +3,9 @@
 import (
 	"errors"
 	"os"
-	"path"
 	"strings"
 
+	securejoin "github.com/cyphar/filepath-securejoin"
 	"github.com/go-git/go-git/v6"
 	"nmyk.io/bidet/internal/core"
 )
@@ -35,15 +35,23 @@ 	}
 	return repos, nil
 }
 
-func (s Server) repoPath(repoName string) string {
-	return path.Join(s.Dir, repoName) + dotGit
+func (s Server) repoPath(repoName string) (string, error) {
+	cleanPath, err := securejoin.SecureJoin(s.Dir, repoName)
+	if err != nil {
+		return "", err
+	}
+	return cleanPath + dotGit, nil
 }
 
 func (s Server) Open(repoName string) (*core.Repo, error) {
-	if _, err := os.Stat(s.repoPath(repoName)); os.IsNotExist(err) {
+	repoPath, err := s.repoPath(repoName)
+	if err != nil {
 		return nil, err
 	}
-	repo, err := git.PlainOpen(s.repoPath(repoName))
+	if _, err := os.Stat(repoPath); os.IsNotExist(err) {
+		return nil, err
+	}
+	repo, err := git.PlainOpen(repoPath)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 85ba4192e852a173598c5bdf8165902149dcc049..cb60b2659979f86536294aa08cd86f4a14182d05 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -339,7 +339,12 @@ 	ctx, cancel := context.WithCancel(r.Context())
 	defer cancel()
 
 	cmd := exec.CommandContext(ctx, "git", args...)
-	cmd.Dir = s.repoPath(repoName)
+	repoPath, err := s.repoPath(repoName)
+	if err != nil {
+		http.NotFound(w, r)
+		return
+	}
+	cmd.Dir = repoPath
 	stdout, err := cmd.StdoutPipe()
 	if err != nil {
 		s.error(w, err)
@@ -452,7 +457,7 @@ func (s Server) Commit(w http.ResponseWriter, r *http.Request) {
 	repoName := r.PathValue("name")
 	repo, err := s.Open(repoName)
 	if err != nil {
-		s.error(w, err)
+		http.NotFound(w, r)
 		return
 	}
 	hashStr := r.PathValue("hash")
@@ -509,7 +514,7 @@ func (s Server) Blob(w http.ResponseWriter, r *http.Request) {
 	repoName := r.PathValue("name")
 	repo, err := s.Open(repoName)
 	if err != nil {
-		s.error(w, err)
+		http.NotFound(w, r)
 		return
 	}
 	commit, filePath, err := repo.ParseRoute(r.PathValue("path"))
@@ -566,7 +571,7 @@ func (s Server) Tree(w http.ResponseWriter, r *http.Request) {
 	repoName := r.PathValue("name")
 	repo, err := s.Open(repoName)
 	if err != nil {
-		s.error(w, err)
+		http.NotFound(w, r)
 		return
 	}
 	urlPath := r.PathValue("path")
@@ -676,15 +681,20 @@ }
 
 func (s Server) InfoRefs(w http.ResponseWriter, r *http.Request) {
 	repoDir := r.PathValue("name")
-	// repoDir already ends in .git
-	repoLocation, cut := strings.CutSuffix(s.repoPath(repoDir), dotGit)
+	repoPath, err := s.repoPath(repoDir)
+	if err != nil {
+		http.NotFound(w, r)
+		return
+	}
+	// repoPath already ends in .git
+	repoLocation, cut := strings.CutSuffix(repoPath, dotGit)
 	if !cut {
 		http.NotFound(w, r)
 		return
 	}
 	repo, err := git.PlainOpen(repoLocation)
 	if err != nil {
-		s.error(w, err)
+		http.NotFound(w, r)
 		return
 	}
 	storage := repo.Storer
@@ -700,8 +710,13 @@ }
 
 func (s Server) UploadPack(w http.ResponseWriter, r *http.Request) {
 	repoDir := r.PathValue("name")
-	// repoDir already ends in .git
-	repoLocation, cut := strings.CutSuffix(s.repoPath(repoDir), dotGit)
+	repoPath, err := s.repoPath(repoDir)
+	if err != nil {
+		http.NotFound(w, r)
+		return
+	}
+	// repoPath already ends in .git
+	repoLocation, cut := strings.CutSuffix(repoPath, dotGit)
 	if !cut {
 		http.NotFound(w, r)
 		return
@@ -732,7 +747,7 @@ func (s Server) RepoTree(w http.ResponseWriter, r *http.Request) {
 	repoName := r.PathValue("name")
 	repo, err := s.Open(repoName)
 	if err != nil {
-		s.error(w, err)
+		http.NotFound(w, r)
 		return
 	}
 	ref, err := repo.Head()