Files
kandev/apps/backend/internal/common/securityutil/git.go
T
2026-08-20 21:20:47 +01:00

147 lines
5.0 KiB
Go

package securityutil
import (
"fmt"
"regexp"
"strings"
)
// validBranchNameRegex matches safe git branch names.
// Allows alphanumeric, hyphens, underscores, slashes, and dots.
// Disallows: spaces, shell metacharacters, and control characters.
var validBranchNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._/-]*$`)
// IsValidBranchName validates that a branch name is safe to use in git commands.
// Returns true if the branch name:
// - Is not empty and under 256 characters
// - Matches the alphanumeric pattern with safe special chars
// - Does not contain ".." (path traversal prevention)
// - Does not end with ".lock" (git internal file)
func IsValidBranchName(branch string) bool {
if branch == "" || len(branch) > 255 {
return false
}
// Disallow ".." to prevent path traversal
if strings.Contains(branch, "..") {
return false
}
// Disallow ending with ".lock"
if strings.HasSuffix(branch, ".lock") {
return false
}
return validBranchNameRegex.MatchString(branch)
}
// IsValidBaseBranchRef validates a base-branch ref using the IsValidBranchName
// allowlist, transparently stripping an "origin/" prefix (the regex disallows
// "/" as the first character). Shared so service-tier base-branch rejection
// uses the same allowlist as task base-branch overrides and the agentctl-side
// sanitiser.
func IsValidBaseBranchRef(ref string) bool {
rest := ref
if stripped, ok := strings.CutPrefix(ref, "origin/"); ok {
rest = stripped
}
// git check-ref-format also rejects a trailing "/" and consecutive "//",
// which the IsValidBranchName regex (its char class includes "/") permits.
// Reject them here so "main/" / "origin/main/" / "a//b" don't pass.
if strings.HasSuffix(rest, "/") || strings.Contains(rest, "//") {
return false
}
return IsValidBranchName(rest)
}
// IsKnownSafeGitFlag returns true if the argument is a known safe git flag used by this codebase.
// This prevents argument injection where user input could introduce malicious flags.
// Only flags actually used by the Kandev codebase are whitelisted.
func IsKnownSafeGitFlag(arg string) bool {
// Flags allowed only as an exact argument, because the prefix match below
// would admit variants this codebase never issues: "--rebase" would let
// through "--rebase-merges" and "--rebase=interactive" (which spawns an
// editor), and "--abort" would let through "--abort-*".
exactFlags := []string{
"--", // Path separator - everything after this is treated as paths, not flags
"--rebase",
"--abort",
}
for _, safe := range exactFlags {
if arg == safe {
return true
}
}
// Whitelist of git flags actually used by our codebase
safeFlags := []string{
"-m", "-M", "-n", "--set-upstream", "--all", "--porcelain", "--short",
"--abbrev-ref", "--symbolic-full-name", "--verify", "--no-patch",
"--format", "--format=", "--stat", "--shortstat", "--numstat", "-p", "-A",
"--amend", "--allow-empty", "--soft", "--mixed", "--hard",
"--cached", "--force", "--source=HEAD", "--staged", "--worktree",
"--dry-run", "--get-all", "--first-parent", "--is-ancestor",
"--src-prefix=", "--dst-prefix=",
}
for _, safe := range safeFlags {
if strings.HasPrefix(arg, safe) {
return true
}
}
return false
}
// IsKnownSafeGitLiteral returns true for git-specific literals that are safe to use in commands.
// This includes git refs, remotes, and special markers that don't need branch name validation.
func IsKnownSafeGitLiteral(arg string) bool {
// Common safe git literals
safeLiterals := []string{
"HEAD", "ORIG_HEAD", "FETCH_HEAD", "MERGE_HEAD",
"origin", "upstream", ".", "--",
}
for _, safe := range safeLiterals {
if arg == safe {
return true
}
}
// HEAD~N, HEAD^N, HEAD@{} patterns are safe
if strings.HasPrefix(arg, "HEAD~") || strings.HasPrefix(arg, "HEAD^") || strings.HasPrefix(arg, "HEAD@{") {
return true
}
return false
}
// LooksLikeCommitSHA returns true if the string looks like a git commit SHA (hex chars only).
// This allows commit SHAs to bypass branch name validation.
// Valid commit SHAs are 7-64 hexadecimal characters (short or full SHA).
func LooksLikeCommitSHA(arg string) bool {
if len(arg) < 7 || len(arg) > 64 {
return false
}
for _, c := range arg {
if !isHexChar(c) {
return false
}
}
return true
}
// ValidateBranchReference validates a branch reference like "origin/branch".
// Returns an error if the reference contains invalid branch or remote names.
func ValidateBranchReference(arg string) error {
parts := strings.SplitN(arg, "/", 2)
if len(parts) != 2 {
return nil // Not a reference format
}
remote, branch := parts[0], parts[1]
if !IsValidBranchName(remote) {
return fmt.Errorf("invalid remote name in reference '%s'", arg)
}
if !IsValidBranchName(branch) {
return fmt.Errorf("invalid branch name in reference '%s'", arg)
}
return nil
}
// isHexChar returns true if the rune is a hexadecimal character (0-9, a-f, A-F).
func isHexChar(r rune) bool {
return (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')
}