mirror of
https://github.com/moby/buildkit.git
synced 2026-11-06 05:16:45 +00:00
git: normalize and validate subdir paths
Normalize Git subdir fragments and validate checkout subdir components so each segment must be a real directory, preventing traversal and symlink escapes. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com> (cherry picked from commit 8c994eb561a2646b35352e5663afecd225306214)
This commit is contained in:
5 files changed
+110
-12
No files matched your search
@@ -398,6 +398,8 @@ func Git(url, fragment string, opts ...GitOption) State {
|
||||
AuthTokenSecret: GitAuthTokenKey,
|
||||
}
|
||||
ref, subdir, ok := strings.Cut(fragment, ":")
|
||||
subdir = path.Join("/", subdir)
|
||||
subdir = strings.TrimPrefix(subdir, "/")
|
||||
if ref != "" {
|
||||
GitRef(ref).SetGitOption(gi)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/moby/buildkit/solver/llbsolver/provenance"
|
||||
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
|
||||
"github.com/moby/buildkit/source"
|
||||
@@ -46,9 +44,6 @@ func NewGitIdentifier(remoteURL string) (*GitIdentifier, error) {
|
||||
repo.Ref = u.Opts.Ref
|
||||
repo.Subdir = u.Opts.Subdir
|
||||
}
|
||||
if sd := path.Clean(repo.Subdir); sd == "/" || sd == "." {
|
||||
repo.Subdir = ""
|
||||
}
|
||||
return &repo, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -48,14 +48,14 @@ func TestNewGitIdentifier(t *testing.T) {
|
||||
expected: GitIdentifier{
|
||||
Remote: "git://github.com/user/repo.git",
|
||||
Ref: "mybranch",
|
||||
Subdir: "mydir/mysubdir/",
|
||||
Subdir: "mydir/mysubdir",
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "git://github.com/user/repo.git#:mydir/mysubdir/",
|
||||
expected: GitIdentifier{
|
||||
Remote: "git://github.com/user/repo.git",
|
||||
Subdir: "mydir/mysubdir/",
|
||||
Subdir: "mydir/mysubdir",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -69,7 +69,7 @@ func TestNewGitIdentifier(t *testing.T) {
|
||||
expected: GitIdentifier{
|
||||
Remote: "https://github.com/user/repo.git",
|
||||
Ref: "mybranch",
|
||||
Subdir: "mydir/mysubdir/",
|
||||
Subdir: "mydir/mysubdir",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -83,7 +83,7 @@ func TestNewGitIdentifier(t *testing.T) {
|
||||
expected: GitIdentifier{
|
||||
Remote: "git@github.com:user/repo.git",
|
||||
Ref: "mybranch",
|
||||
Subdir: "mydir/mysubdir/",
|
||||
Subdir: "mydir/mysubdir",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -97,7 +97,7 @@ func TestNewGitIdentifier(t *testing.T) {
|
||||
expected: GitIdentifier{
|
||||
Remote: "ssh://github.com/user/repo.git",
|
||||
Ref: "mybranch",
|
||||
Subdir: "mydir/mysubdir/",
|
||||
Subdir: "mydir/mysubdir",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -105,7 +105,70 @@ func TestNewGitIdentifier(t *testing.T) {
|
||||
expected: GitIdentifier{
|
||||
Remote: "ssh://foo%40barcorp.com@github.com/user/repo.git",
|
||||
Ref: "mybranch",
|
||||
Subdir: "mydir/mysubdir/",
|
||||
Subdir: "mydir/mysubdir",
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "https://github.com/user/repo.git#main:../../escape",
|
||||
expected: GitIdentifier{
|
||||
Remote: "https://github.com/user/repo.git",
|
||||
Ref: "main",
|
||||
Subdir: "escape",
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "https://github.com/user/repo.git#main:dir/../../escape",
|
||||
expected: GitIdentifier{
|
||||
Remote: "https://github.com/user/repo.git",
|
||||
Ref: "main",
|
||||
Subdir: "escape",
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "https://github.com/user/repo.git#main:/absolute/path",
|
||||
expected: GitIdentifier{
|
||||
Remote: "https://github.com/user/repo.git",
|
||||
Ref: "main",
|
||||
Subdir: "absolute/path",
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "https://github.com/user/repo.git#main:../",
|
||||
expected: GitIdentifier{
|
||||
Remote: "https://github.com/user/repo.git",
|
||||
Ref: "main",
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "ssh://github.com/user/repo.git#main:../../escape",
|
||||
expected: GitIdentifier{
|
||||
Remote: "ssh://github.com/user/repo.git",
|
||||
Ref: "main",
|
||||
Subdir: "escape",
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "ssh://github.com/user/repo.git#main:/absolute/path",
|
||||
expected: GitIdentifier{
|
||||
Remote: "ssh://github.com/user/repo.git",
|
||||
Ref: "main",
|
||||
Subdir: "absolute/path",
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "git@github.com:user/repo.git#main:../../escape",
|
||||
expected: GitIdentifier{
|
||||
Remote: "git@github.com:user/repo.git",
|
||||
Ref: "main",
|
||||
Subdir: "escape",
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "git@github.com:user/repo.git#main:/absolute/path",
|
||||
expected: GitIdentifier{
|
||||
Remote: "git@github.com:user/repo.git",
|
||||
Ref: "main",
|
||||
Subdir: "absolute/path",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+36
-1
@@ -993,7 +993,7 @@ func (gs *gitSourceHandler) checkout(ctx context.Context, repo *gitRepo, g sessi
|
||||
}
|
||||
}()
|
||||
|
||||
subdir := path.Clean(gs.src.Subdir)
|
||||
subdir := path.Join("/", gs.src.Subdir)
|
||||
if subdir == "/" {
|
||||
subdir = "."
|
||||
}
|
||||
@@ -1086,6 +1086,11 @@ func (gs *gitSourceHandler) checkout(ctx context.Context, repo *gitRepo, g sessi
|
||||
}
|
||||
|
||||
if subdir != "." {
|
||||
subdir = filepath.FromSlash(subdir)
|
||||
if err := validateDirsOnly(cd, subdir); err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid subdir %v", subdir)
|
||||
}
|
||||
|
||||
d, err := os.Open(filepath.Join(cd, subdir))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to open subdir %v", subdir)
|
||||
@@ -1290,3 +1295,33 @@ func gitCLI(opts ...gitutil.Option) *gitutil.GitCLI {
|
||||
}, opts...)
|
||||
return gitutil.NewGitCLI(opts...)
|
||||
}
|
||||
|
||||
// validateDirsOnly checks that the given subpath in the repository
|
||||
// only contains directories without any symlinks or files.
|
||||
func validateDirsOnly(root string, subpath string) error {
|
||||
rel := filepath.Clean(subpath)
|
||||
rel = strings.TrimPrefix(rel, string(filepath.Separator))
|
||||
if rel == "" || rel == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
r, err := os.OpenRoot(root)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open root %q", root)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
p := ""
|
||||
for part := range strings.SplitSeq(rel, string(filepath.Separator)) {
|
||||
p = filepath.Join(p, part)
|
||||
|
||||
fi, err := r.Lstat(p)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to lstat %q", p)
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return errors.Errorf("git subpath %q contains non-directory %q", subpath, p)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package gitutil
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -72,6 +73,8 @@ func parseOpts(fragment string) *GitURLOpts {
|
||||
return nil
|
||||
}
|
||||
ref, subdir, _ := strings.Cut(fragment, ":")
|
||||
subdir = path.Join("/", subdir)
|
||||
subdir = strings.TrimPrefix(subdir, "/")
|
||||
return &GitURLOpts{Ref: ref, Subdir: subdir}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user