Show 85 hidden lines
+ "fmt"+ "os"+ "path/filepath"+ "sort"+ "strings"+ "testing"++ billy "github.com/go-git/go-billy/v5"+ "github.com/go-git/go-billy/v5/memfs"+ "github.com/go-git/go-billy/v5/osfs"+ "github.com/go-git/go-billy/v5/util"+ "github.com/stretchr/testify/assert"+ "github.com/stretchr/testify/require"++ "github.com/go-git/go-git/v5/plumbing"+ "github.com/go-git/go-git/v5/plumbing/filemode"+ "github.com/go-git/go-git/v5/plumbing/object"+ "github.com/go-git/go-git/v5/plumbing/storer"+)++// TestForceCheckoutReplacesLeadingSymlink covers the symlink-mask bypass+// (CVE-2021-21300 class). A symlink "s" pointing at a dangerous directory+// is present in the worktree, planted by an attacker or left by an earlier+// checkout step, and the commit being force-checked-out writes a file at+// "s/<leaf>". That path string is innocent (no ".git" or ".." component),+// so string validation alone lets it through. Following the symlink would+// let the write escape the worktree.+//+// Matching upstream Git's create_directories, a force checkout must remove+// the blocking symlink and materialise a real directory, writing the file+// safely inside the worktree. Each case asserts the checkout succeeds, the+// dangerous target is untouched, and the file lands in the worktree+// instead.+func TestForceCheckoutReplacesLeadingSymlink(t *testing.T) {+ if testing.Short() {+ t.Skip("skipping symlink-mask path validation test in short mode")+ }+ t.Parallel()++ const exploit = "exploit"++ cases := []struct {+ name string+ // setup returns the symlink target (the on-disk contents of the+ // planted symlink "s"), the tree path the attack commit writes,+ // and the on-disk location that path resolves to if "s" is+ // followed.+ setup func(t *testing.T, repoDir string) (linkTarget, entryPath, escapePath string)+ }{+ {+ name: "masks .git config",+ setup: func(_ *testing.T, repoDir string) (string, string, string) {+ return ".git", "s/config", filepath.Join(repoDir, ".git", "config")+ },+ },+ {+ name: "masks .git hook",+ setup: func(_ *testing.T, repoDir string) (string, string, string) {+ return ".git", "s/hooks/pre-commit", filepath.Join(repoDir, ".git", "hooks", "pre-commit")+ },+ },+ {+ name: "masks parent dir",+ setup: func(_ *testing.T, repoDir string) (string, string, string) {+ return "..", "s/escape", filepath.Join(filepath.Dir(repoDir), "escape")+ },+ },+ {+ name: "masks absolute external dir",+ setup: func(t *testing.T, _ string) (string, string, string) {+ escape := t.TempDir()+ return escape, "s/escape", filepath.Join(escape, "escape")+ },+ },+ }++ for _, tc := range cases {+ t.Run(tc.name, func(t *testing.T) {+ t.Parallel()++ dir := t.TempDir()++ r, err := PlainInit(dir, false)+ require.NoError(t, err)+
Adds end-to-end forced checkout tests that plant leading symlinks to .git, parent directories, and absolute outside paths, then require replacement by real directories with the target file untouched.