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()