VYPR
Moderate severityNVD Advisory· Published Jan 8, 2025· Updated Jan 8, 2025

Soft Serve allows path traversal attacks

CVE-2025-22130

Description

Soft Serve is a self-hostable Git server for the command line. Prior to 0.8.2 , a path traversal attack allows existing non-admin users to access and take over other user's repositories. A malicious user then can modify, delete, and arbitrarily repositories as if they were an admin user without explicitly giving them permissions. This is patched in v0.8.2.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/charmbracelet/soft-serveGo
< 0.8.20.8.2

Affected products

1

Patches

1
a8d1bf3f9349

fix: prevent path traversal attacks (#631)

https://github.com/charmbracelet/soft-serveAyman BagabasJan 7, 2025via ghsa
3 files changed · +23 21
  • pkg/backend/repo.go+18 19 modified
    @@ -10,6 +10,7 @@ import (
     	"path"
     	"path/filepath"
     	"strconv"
    +	"strings"
     	"time"
     
     	"github.com/charmbracelet/soft-serve/git"
    @@ -24,10 +25,6 @@ import (
     	"github.com/charmbracelet/soft-serve/pkg/webhook"
     )
     
    -func (d *Backend) reposPath() string {
    -	return filepath.Join(d.cfg.DataPath, "repos")
    -}
    -
     // CreateRepository creates a new repository.
     //
     // It implements backend.Backend.
    @@ -37,8 +34,7 @@ func (d *Backend) CreateRepository(ctx context.Context, name string, user proto.
     		return nil, err
     	}
     
    -	repo := name + ".git"
    -	rp := filepath.Join(d.reposPath(), repo)
    +	rp := filepath.Join(d.repoPath(name))
     
     	var userID int64
     	if user != nil {
    @@ -78,7 +74,7 @@ func (d *Backend) CreateRepository(ctx context.Context, name string, user proto.
     			}
     		}
     
    -		return hooks.GenerateHooks(ctx, d.cfg, repo)
    +		return hooks.GenerateHooks(ctx, d.cfg, name)
     	}); err != nil {
     		d.logger.Debug("failed to create repository in database", "err", err)
     		err = db.WrapError(err)
    @@ -100,8 +96,7 @@ func (d *Backend) ImportRepository(_ context.Context, name string, user proto.Us
     		return nil, err
     	}
     
    -	repo := name + ".git"
    -	rp := filepath.Join(d.reposPath(), repo)
    +	rp := filepath.Join(d.repoPath(name))
     
     	tid := "import:" + name
     	if d.manager.Exists(tid) {
    @@ -217,8 +212,7 @@ func (d *Backend) ImportRepository(_ context.Context, name string, user proto.Us
     // It implements backend.Backend.
     func (d *Backend) DeleteRepository(ctx context.Context, name string) error {
     	name = utils.SanitizeRepo(name)
    -	repo := name + ".git"
    -	rp := filepath.Join(d.reposPath(), repo)
    +	rp := filepath.Join(d.repoPath(name))
     
     	user := proto.UserFromContext(ctx)
     	r, err := d.Repository(ctx, name)
    @@ -330,10 +324,8 @@ func (d *Backend) RenameRepository(ctx context.Context, oldName string, newName
     		return nil
     	}
     
    -	oldRepo := oldName + ".git"
    -	newRepo := newName + ".git"
    -	op := filepath.Join(d.reposPath(), oldRepo)
    -	np := filepath.Join(d.reposPath(), newRepo)
    +	op := filepath.Join(d.repoPath(oldName))
    +	np := filepath.Join(d.repoPath(newName))
     	if _, err := os.Stat(op); err != nil {
     		return proto.ErrRepoNotFound
     	}
    @@ -389,7 +381,7 @@ func (d *Backend) Repositories(ctx context.Context) ([]proto.Repository, error)
     		for _, m := range ms {
     			r := &repo{
     				name: m.Name,
    -				path: filepath.Join(d.reposPath(), m.Name+".git"),
    +				path: filepath.Join(d.repoPath(m.Name)),
     				repo: m,
     			}
     
    @@ -418,7 +410,7 @@ func (d *Backend) Repository(ctx context.Context, name string) (proto.Repository
     		return r, nil
     	}
     
    -	rp := filepath.Join(d.reposPath(), name+".git")
    +	rp := filepath.Join(d.repoPath(name))
     	if _, err := os.Stat(rp); err != nil {
     		if !errors.Is(err, fs.ErrNotExist) {
     			d.logger.Errorf("failed to stat repository path: %v", err)
    @@ -552,7 +544,7 @@ func (d *Backend) SetHidden(ctx context.Context, name string, hidden bool) error
     // It implements backend.Backend.
     func (d *Backend) SetDescription(ctx context.Context, name string, desc string) error {
     	name = utils.SanitizeRepo(name)
    -	rp := filepath.Join(d.reposPath(), name+".git")
    +	rp := filepath.Join(d.repoPath(name))
     
     	// Delete cache
     	d.cache.Delete(name)
    @@ -572,7 +564,7 @@ func (d *Backend) SetDescription(ctx context.Context, name string, desc string)
     // It implements backend.Backend.
     func (d *Backend) SetPrivate(ctx context.Context, name string, private bool) error {
     	name = utils.SanitizeRepo(name)
    -	rp := filepath.Join(d.reposPath(), name+".git")
    +	rp := filepath.Join(d.repoPath(name))
     
     	// Delete cache
     	d.cache.Delete(name)
    @@ -636,6 +628,13 @@ func (d *Backend) SetProjectName(ctx context.Context, repo string, name string)
     	)
     }
     
    +// repoPath returns the path to a repository.
    +func (d *Backend) repoPath(name string) string {
    +	name = utils.SanitizeRepo(name)
    +	rn := strings.ReplaceAll(name, "/", string(os.PathSeparator))
    +	return filepath.Join(filepath.Join(d.cfg.DataPath, "repos"), rn+".git")
    +}
    +
     var _ proto.Repository = (*repo)(nil)
     
     // repo is a Git repository with metadata stored in a SQLite database.
    
  • pkg/ssh/cmd/cmd.go+1 1 modified
    @@ -172,7 +172,7 @@ func checkIfAdmin(cmd *cobra.Command, args []string) error {
     func checkIfCollab(cmd *cobra.Command, args []string) error {
     	var repo string
     	if len(args) > 0 {
    -		repo = args[0]
    +		repo = utils.SanitizeRepo(args[0])
     	}
     
     	ctx := cmd.Context()
    
  • pkg/utils/utils.go+4 1 modified
    @@ -9,12 +9,15 @@ import (
     
     // SanitizeRepo returns a sanitized version of the given repository name.
     func SanitizeRepo(repo string) string {
    +	// We need to use an absolute path for the path to be cleaned correctly.
     	repo = strings.TrimPrefix(repo, "/")
    +	repo = "/" + repo
    +
     	// We're using path instead of filepath here because this is not OS dependent
     	// looking at you Windows
     	repo = path.Clean(repo)
     	repo = strings.TrimSuffix(repo, ".git")
    -	return repo
    +	return repo[1:]
     }
     
     // ValidateUsername returns an error if any of the given usernames are invalid.
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.