Skip to content

Commit a8d1bf3

Browse files
authored
fix: prevent path traversal attacks (#631)
This commit fixes a path traversal vulnerability in the repository management code. The `SanitizeRepo` function now correctly returns a sanitized version of the given repository name. It uses an absolute path along with `path.Clean` to ensure that the path is cleaned before being used.
1 parent 0fb868c commit a8d1bf3

File tree

3 files changed

+23
-21
lines changed

3 files changed

+23
-21
lines changed

pkg/backend/repo.go

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"path"
1111
"path/filepath"
1212
"strconv"
13+
"strings"
1314
"time"
1415

1516
"github.com/charmbracelet/soft-serve/git"
@@ -24,10 +25,6 @@ import (
2425
"github.com/charmbracelet/soft-serve/pkg/webhook"
2526
)
2627

27-
func (d *Backend) reposPath() string {
28-
return filepath.Join(d.cfg.DataPath, "repos")
29-
}
30-
3128
// CreateRepository creates a new repository.
3229
//
3330
// It implements backend.Backend.
@@ -37,8 +34,7 @@ func (d *Backend) CreateRepository(ctx context.Context, name string, user proto.
3734
return nil, err
3835
}
3936

40-
repo := name + ".git"
41-
rp := filepath.Join(d.reposPath(), repo)
37+
rp := filepath.Join(d.repoPath(name))
4238

4339
var userID int64
4440
if user != nil {
@@ -78,7 +74,7 @@ func (d *Backend) CreateRepository(ctx context.Context, name string, user proto.
7874
}
7975
}
8076

81-
return hooks.GenerateHooks(ctx, d.cfg, repo)
77+
return hooks.GenerateHooks(ctx, d.cfg, name)
8278
}); err != nil {
8379
d.logger.Debug("failed to create repository in database", "err", err)
8480
err = db.WrapError(err)
@@ -100,8 +96,7 @@ func (d *Backend) ImportRepository(_ context.Context, name string, user proto.Us
10096
return nil, err
10197
}
10298

103-
repo := name + ".git"
104-
rp := filepath.Join(d.reposPath(), repo)
99+
rp := filepath.Join(d.repoPath(name))
105100

106101
tid := "import:" + name
107102
if d.manager.Exists(tid) {
@@ -217,8 +212,7 @@ func (d *Backend) ImportRepository(_ context.Context, name string, user proto.Us
217212
// It implements backend.Backend.
218213
func (d *Backend) DeleteRepository(ctx context.Context, name string) error {
219214
name = utils.SanitizeRepo(name)
220-
repo := name + ".git"
221-
rp := filepath.Join(d.reposPath(), repo)
215+
rp := filepath.Join(d.repoPath(name))
222216

223217
user := proto.UserFromContext(ctx)
224218
r, err := d.Repository(ctx, name)
@@ -330,10 +324,8 @@ func (d *Backend) RenameRepository(ctx context.Context, oldName string, newName
330324
return nil
331325
}
332326

333-
oldRepo := oldName + ".git"
334-
newRepo := newName + ".git"
335-
op := filepath.Join(d.reposPath(), oldRepo)
336-
np := filepath.Join(d.reposPath(), newRepo)
327+
op := filepath.Join(d.repoPath(oldName))
328+
np := filepath.Join(d.repoPath(newName))
337329
if _, err := os.Stat(op); err != nil {
338330
return proto.ErrRepoNotFound
339331
}
@@ -389,7 +381,7 @@ func (d *Backend) Repositories(ctx context.Context) ([]proto.Repository, error)
389381
for _, m := range ms {
390382
r := &repo{
391383
name: m.Name,
392-
path: filepath.Join(d.reposPath(), m.Name+".git"),
384+
path: filepath.Join(d.repoPath(m.Name)),
393385
repo: m,
394386
}
395387

@@ -418,7 +410,7 @@ func (d *Backend) Repository(ctx context.Context, name string) (proto.Repository
418410
return r, nil
419411
}
420412

421-
rp := filepath.Join(d.reposPath(), name+".git")
413+
rp := filepath.Join(d.repoPath(name))
422414
if _, err := os.Stat(rp); err != nil {
423415
if !errors.Is(err, fs.ErrNotExist) {
424416
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
552544
// It implements backend.Backend.
553545
func (d *Backend) SetDescription(ctx context.Context, name string, desc string) error {
554546
name = utils.SanitizeRepo(name)
555-
rp := filepath.Join(d.reposPath(), name+".git")
547+
rp := filepath.Join(d.repoPath(name))
556548

557549
// Delete cache
558550
d.cache.Delete(name)
@@ -572,7 +564,7 @@ func (d *Backend) SetDescription(ctx context.Context, name string, desc string)
572564
// It implements backend.Backend.
573565
func (d *Backend) SetPrivate(ctx context.Context, name string, private bool) error {
574566
name = utils.SanitizeRepo(name)
575-
rp := filepath.Join(d.reposPath(), name+".git")
567+
rp := filepath.Join(d.repoPath(name))
576568

577569
// Delete cache
578570
d.cache.Delete(name)
@@ -636,6 +628,13 @@ func (d *Backend) SetProjectName(ctx context.Context, repo string, name string)
636628
)
637629
}
638630

631+
// repoPath returns the path to a repository.
632+
func (d *Backend) repoPath(name string) string {
633+
name = utils.SanitizeRepo(name)
634+
rn := strings.ReplaceAll(name, "/", string(os.PathSeparator))
635+
return filepath.Join(filepath.Join(d.cfg.DataPath, "repos"), rn+".git")
636+
}
637+
639638
var _ proto.Repository = (*repo)(nil)
640639

641640
// repo is a Git repository with metadata stored in a SQLite database.

pkg/ssh/cmd/cmd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ func checkIfAdmin(cmd *cobra.Command, args []string) error {
172172
func checkIfCollab(cmd *cobra.Command, args []string) error {
173173
var repo string
174174
if len(args) > 0 {
175-
repo = args[0]
175+
repo = utils.SanitizeRepo(args[0])
176176
}
177177

178178
ctx := cmd.Context()

pkg/utils/utils.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ import (
99

1010
// SanitizeRepo returns a sanitized version of the given repository name.
1111
func SanitizeRepo(repo string) string {
12+
// We need to use an absolute path for the path to be cleaned correctly.
1213
repo = strings.TrimPrefix(repo, "/")
14+
repo = "/" + repo
15+
1316
// We're using path instead of filepath here because this is not OS dependent
1417
// looking at you Windows
1518
repo = path.Clean(repo)
1619
repo = strings.TrimSuffix(repo, ".git")
17-
return repo
20+
return repo[1:]
1821
}
1922

2023
// ValidateUsername returns an error if any of the given usernames are invalid.

0 commit comments

Comments
 (0)