fierj
PublicTiny personal git forge
897797d654e54cabc557bae5dad581023e4ffe17
diff --git a/git.go b/git.go
index 0ff67d7..dc9fdc4 100644
--- a/git.go
+++ b/git.go
@@ -143,6 +143,51 @@ type Git struct {
var _ VCS = (*Git)(nil)
+// isValidRef checks that a branch, tag, or ref name contains only safe characters
+// and won't be interpreted as a flag or cause path traversal.
+func isValidRef(ref string) bool {
+ if ref == "" || strings.HasPrefix(ref, "-") || strings.Contains(ref, "..") {
+ return false
+ }
+ for _, r := range ref {
+ if r <= ' ' || r == 0x7f || r == '~' || r == '^' || r == ':' || r == '\\' {
+ return false
+ }
+ }
+ for _, part := range strings.Split(ref, "/") {
+ if part == "" || part == "." || part == ".." || strings.HasPrefix(part, ".") {
+ return false
+ }
+ }
+ return true
+}
+
+// isValidPath checks that a path within a repository is safe.
+func isValidPath(p string) bool {
+ if strings.HasPrefix(p, "/") || strings.Contains(p, "..") {
+ return false
+ }
+ for _, r := range p {
+ if r <= ' ' || r == 0x7f || r == ':' || r == '\\' {
+ return false
+ }
+ }
+ return true
+}
+
+// isValidHash checks that a string looks like a git commit hash.
+func isValidHash(h string) bool {
+ if len(h) < 7 || len(h) > 40 {
+ return false
+ }
+ for _, r := range h {
+ if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) {
+ return false
+ }
+ }
+ return true
+}
+
func (g *Git) cmd(args ...string) (string, error) {
cmd := exec.Command("git", args...)
cmd.Dir = path.Join(g.Dir, g.Name+".git")
@@ -256,6 +301,9 @@ func (g *Git) IsPrivate() bool {
}
func (g *Git) List(ref, path string) ([]TreeEntry, error) {
+ if !isValidRef(ref) || (path != "" && !isValidPath(path)) {
+ return nil, fmt.Errorf("invalid ref or path")
+ }
args := []string{"ls-tree", "-l", ref}
if path != "" {
args = append(args, "--", path+"/")
@@ -294,12 +342,18 @@ func (g *Git) List(ref, path string) ([]TreeEntry, error) {
}
func (g *Git) Blob(ref, path string) (string, error) {
+ if !isValidRef(ref) || !isValidPath(path) {
+ return "", fmt.Errorf("invalid ref or path")
+ }
return g.cmd("show", ref+":"+path)
}
func (g *Git) Log(ref string, n int) ([]Commit, error) {
+ if !isValidRef(ref) {
+ return nil, fmt.Errorf("invalid ref")
+ }
format := "%H%n%an%n%aI%n%s%n---"
- out, err := g.cmd("log", fmt.Sprintf("-%d", n), "--format="+format, ref)
+ out, err := g.cmd("log", fmt.Sprintf("-%d", n), "--format="+format, "--", ref)
if err != nil {
return nil, err
}
@@ -321,17 +375,20 @@ func (g *Git) Log(ref string, n int) ([]Commit, error) {
}
func (g *Git) Diff(hash string) (string, error) {
- return g.cmd("diff-tree", "-p", hash)
+ if !isValidHash(hash) {
+ return "", fmt.Errorf("invalid hash")
+ }
+ return g.cmd("diff-tree", "-p", "--", hash)
}
func (g *Git) DefaultBranch() string {
out, err := g.cmd("symbolic-ref", "--short", "HEAD")
if err == nil {
ref := strings.TrimSpace(out)
- // Verify the default branch actually exists (HEAD might point to
- // a not-yet-created branch in an empty/dangling-HEAD repo).
- if _, err2 := g.cmd("rev-parse", "--verify", ref); err2 == nil {
- return ref
+ if isValidRef(ref) {
+ if _, err2 := g.cmd("rev-parse", "--verify", "--", ref); err2 == nil {
+ return ref
+ }
}
}
// Fall back to the first real branch, or "main" as last resort.
@@ -379,7 +436,10 @@ func (g *Git) Tags() []string {
}
func (g *Git) CommitCount(ref string) int {
- out, err := g.cmd("rev-list", "--count", ref)
+ if !isValidRef(ref) {
+ return 0
+ }
+ out, err := g.cmd("rev-list", "--count", "--", ref)
if err != nil {
return 0
}
@@ -389,6 +449,9 @@ func (g *Git) CommitCount(ref string) int {
}
func (g *Git) Readme(ref, dir string) string {
+ if !isValidRef(ref) || (dir != "" && !isValidPath(dir)) {
+ return ""
+ }
names := []string{"README.md", "readme.md", "README", "README.txt"}
for _, name := range names {
p := name
@@ -418,7 +481,10 @@ func listBranches(repoPath string) []string {
}
func (g *Git) LastCommit(ref, path string) *Commit {
- out, err := g.cmd("log", "-1", "--format=%H%n%an%n%ar%n%s", ref, "--", path)
+ if !isValidRef(ref) || (path != "" && !isValidPath(path)) {
+ return nil
+ }
+ out, err := g.cmd("log", "-1", "--format=%H%n%an%n%ar%n%s", "--", ref, "--", path)
if err != nil {
return nil
}
diff --git a/patch.go b/patch.go
index 254ada4..b4e85f3 100644
--- a/patch.go
+++ b/patch.go
@@ -151,7 +151,9 @@ func loadPatch(repoPath, patchID string) (*Patch, error) {
// patchDiff returns the diff for a patch (either branch-based or file-based).
func patchDiff(repoPath string, p *Patch) (string, error) {
if p.Branch != "" {
- // Diff between default branch and patch branch
+ if !isValidRef(p.Branch) {
+ return "", fmt.Errorf("invalid branch name")
+ }
defBranch := defaultBranch(repoPath)
cmd := exec.Command("git", "diff", defBranch+"..."+p.Branch)
cmd.Dir = repoPath
@@ -238,6 +240,9 @@ func writePatch(repoPath string, p *Patch) error {
// fastForwardMerge updates target to point to branch if target is an ancestor
// of branch (pure fast-forward). Works in bare repositories.
func fastForwardMerge(repoPath, target, branch string) error {
+ if !isValidRef(target) || !isValidRef(branch) {
+ return fmt.Errorf("invalid branch name")
+ }
// Check that target is an ancestor of branch.
cmd := exec.Command("git", "merge-base", "--is-ancestor", "refs/heads/"+target, "refs/heads/"+branch)
cmd.Dir = repoPath