diff --git a/internal/controlplane/runs.go b/internal/controlplane/runs.go index 55a954b..dc026e3 100644 --- a/internal/controlplane/runs.go +++ b/internal/controlplane/runs.go @@ -236,7 +236,10 @@ func (s *Store) resolveBaseSHA(ctx context.Context, identity, ref string) (strin } ctx, cancel := context.WithTimeout(ctx, protocol.GitCommandTimeout) defer cancel() - command := exec.CommandContext(ctx, "git", "ls-remote", "--exit-code", source, ref) + // "--" ends git's options: a source or ref that starts with "-" is a + // name, never a flag such as --upload-pack, which runs a command. + // Normalization keeps sources off "-" today; this does not rely on it. + command := exec.CommandContext(ctx, "git", "ls-remote", "--exit-code", "--", source, ref) // The control plane's own environment, minus any chance of git stopping // to ask a human for credentials: an admission must fail loudly, never // block on a prompt nobody is watching. diff --git a/internal/controlplane/runs_test.go b/internal/controlplane/runs_test.go index f84e323..bd468f8 100644 --- a/internal/controlplane/runs_test.go +++ b/internal/controlplane/runs_test.go @@ -443,6 +443,25 @@ func TestAdmissionPinsAnExplicitRefAndRefusesOneThatDoesNotResolve(t *testing.T) } } +func TestPinningNeverReadsASourceOrRefAsAGitOption(t *testing.T) { + store, _ := newTestStore(t) + repository, _, _ := newFixtureRepository(t, "options") + marker := filepath.Join(t.TempDir(), "upload-pack-ran") + option := "--upload-pack=touch " + marker + + // A file:// identity reaches CloneSourceForIdentity unnormalized here, so + // the source itself is the option — the case normalization alone guards. + if _, err := store.resolveBaseSHA(context.Background(), "file://"+option, "HEAD"); err == nil { + t.Fatal("a source that is a git option must fail to resolve") + } + if _, err := store.resolveBaseSHA(context.Background(), repository, option); err == nil { + t.Fatal("a ref that is a git option must fail to resolve") + } + if _, err := os.Stat(marker); err == nil { + t.Fatal("git ran --upload-pack: a source or ref was read as an option") + } +} + // ---- repository identity (U3's note) -------------------------------------- func TestAdmissionStoresTheCanonicalRepositoryIdentityOnEveryJob(t *testing.T) {