From b3222c44d17c5432790e43e13b47f7e89309b64b Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Fri, 13 Mar 2026 14:07:40 +0900 Subject: [PATCH] knotserver/git: reject requests to unknown repos Signed-off-by: Seongmin Lee --- knotserver/git.go | 58 ++++++++++++++++++-------------------------- knotserver/router.go | 40 ++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 34 deletions(-) diff --git a/knotserver/git.go b/knotserver/git.go index e2018c46..227ddef4 100644 --- a/knotserver/git.go +++ b/knotserver/git.go @@ -5,28 +5,19 @@ import ( "fmt" "io" "net/http" - "path/filepath" + "os" "strings" - securejoin "github.com/cyphar/filepath-securejoin" "github.com/go-chi/chi/v5" "tangled.org/core/knotserver/git/service" ) func (h *Knot) InfoRefs(w http.ResponseWriter, r *http.Request) { - did := chi.URLParam(r, "did") name := chi.URLParam(r, "name") - repoName, err := securejoin.SecureJoin(did, name) - if err != nil { - gitError(w, "repository not found", http.StatusNotFound) - h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) - return - } - - repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, repoName) - if err != nil { - gitError(w, "repository not found", http.StatusNotFound) - h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) + repoPath, ok := repoPathFromcontext(r.Context()) + if !ok { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Failed to find repository path")) return } @@ -57,12 +48,10 @@ func (h *Knot) InfoRefs(w http.ResponseWriter, r *http.Request) { } func (h *Knot) UploadArchive(w http.ResponseWriter, r *http.Request) { - did := chi.URLParam(r, "did") - name := chi.URLParam(r, "name") - repo, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) - if err != nil { - gitError(w, err.Error(), http.StatusInternalServerError) - h.l.Error("git: failed to secure join repo path", "handler", "UploadPack", "error", err) + repo, ok := repoPathFromcontext(r.Context()) + if !ok { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Failed to find repository path")) return } @@ -104,12 +93,10 @@ func (h *Knot) UploadArchive(w http.ResponseWriter, r *http.Request) { } func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { - did := chi.URLParam(r, "did") - name := chi.URLParam(r, "name") - repo, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) - if err != nil { - gitError(w, err.Error(), http.StatusInternalServerError) - h.l.Error("git: failed to secure join repo path", "handler", "UploadPack", "error", err) + repo, ok := repoPathFromcontext(r.Context()) + if !ok { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Failed to find repository path")) return } @@ -153,15 +140,7 @@ func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { } func (h *Knot) ReceivePack(w http.ResponseWriter, r *http.Request) { - did := chi.URLParam(r, "did") name := chi.URLParam(r, "name") - _, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) - if err != nil { - gitError(w, err.Error(), http.StatusForbidden) - h.l.Error("git: failed to secure join repo path", "handler", "ReceivePack", "error", err) - return - } - h.RejectPush(w, r, name) } @@ -192,6 +171,17 @@ func (h *Knot) RejectPush(w http.ResponseWriter, r *http.Request, unqualifiedRep fmt.Fprintf(w, "\n\n") } +func isDir(path string) (bool, error) { + info, err := os.Stat(path) + if err == nil && info.IsDir() { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + func gitError(w http.ResponseWriter, msg string, status int) { w.Header().Set("content-type", "text/plain; charset=UTF-8") w.WriteHeader(status) diff --git a/knotserver/router.go b/knotserver/router.go index 131fb13b..d290def4 100644 --- a/knotserver/router.go +++ b/knotserver/router.go @@ -5,8 +5,10 @@ import ( "fmt" "log/slog" "net/http" + "path/filepath" "strings" + securejoin "github.com/cyphar/filepath-securejoin" "github.com/go-chi/chi/v5" "tangled.org/core/idresolver" "tangled.org/core/jetstream" @@ -81,6 +83,7 @@ func (h *Knot) Router() http.Handler { r.Route("/{did}", func(r chi.Router) { r.Use(h.resolveDidRedirect) + r.Use(h.resolveRepo) r.Route("/{name}", func(r chi.Router) { // routes for git operations r.Get("/info/refs", h.InfoRefs) @@ -141,6 +144,43 @@ func (h *Knot) resolveDidRedirect(next http.Handler) http.Handler { }) } +type ctxRepoPathKey struct{} + +func repoPathFromcontext(ctx context.Context) (string, bool) { + v, ok := ctx.Value(ctxRepoPathKey{}).(string) + return v, ok +} + +// resolveRepo is a http middleware that constructs git repo path from given did & name pair. +// It will reject the requests to unknown repos (when dir doesn't exist) +func (h *Knot) resolveRepo(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + did := chi.URLParam(r, "did") + name := chi.URLParam(r, "name") + repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) + if err != nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("Repository not found")) + return + } + + exist, err := isDir(repoPath) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Failed to check repository path")) + return + } + if !exist { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("Repository not found")) + return + } + + ctx := context.WithValue(r.Context(), "repoPath", repoPath) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + func (h *Knot) configureOwner() error { cfgOwner := h.c.Server.Owner -- 2.51.2