Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZeger-Jan van de Weg <git@zjvandeweg.nl>2020-03-12 11:14:24 +0300
committerZeger-Jan van de Weg <git@zjvandeweg.nl>2020-03-12 11:14:24 +0300
commit353360039159911250175c939a30f417e7487641 (patch)
tree2768aef4ac009f0efe461475e4473203cad82695
parentf414810c76834bfb3370af0b4b0f46e1f00b58d7 (diff)
parent7438d09ea3a1aacbe150eba5e9b1edd3eeddff18 (diff)
Merge branch '2148-object-pool-path-validation' into 'master'
Validate object pool relative paths Closes #2418 See merge request gitlab-org/gitaly!1900
-rw-r--r--changelogs/unreleased/smh-prevent-path-deletion.yml5
-rw-r--r--internal/git/objectpool/clone.go10
-rw-r--r--internal/git/objectpool/pool.go25
-rw-r--r--internal/service/objectpool/create.go14
-rw-r--r--internal/service/objectpool/create_test.go111
-rw-r--r--internal/service/objectpool/link_test.go2
-rw-r--r--internal/service/repository/clone_from_pool_internal_test.go4
-rw-r--r--internal/testhelper/testhelper.go66
8 files changed, 178 insertions, 59 deletions
diff --git a/changelogs/unreleased/smh-prevent-path-deletion.yml b/changelogs/unreleased/smh-prevent-path-deletion.yml
new file mode 100644
index 000000000..7ae727057
--- /dev/null
+++ b/changelogs/unreleased/smh-prevent-path-deletion.yml
@@ -0,0 +1,5 @@
+---
+title: Validate object pool relative paths to prevent path traversal attacks.
+merge_request: 1900
+author:
+type: security
diff --git a/internal/git/objectpool/clone.go b/internal/git/objectpool/clone.go
index 45ab2af81..ba0febd58 100644
--- a/internal/git/objectpool/clone.go
+++ b/internal/git/objectpool/clone.go
@@ -18,17 +18,11 @@ func (o *ObjectPool) clone(ctx context.Context, repo *gitalypb.Repository) error
return err
}
- targetDir := o.FullPath()
- targetName := path.Base(targetDir)
- if err != nil {
- return err
- }
-
cmd, err := git.SafeCmdWithoutRepo(ctx,
[]git.Option{
git.ValueFlag{
Name: "-C",
- Value: path.Dir(targetDir),
+ Value: o.storagePath,
},
},
git.SubCmd{
@@ -38,7 +32,7 @@ func (o *ObjectPool) clone(ctx context.Context, repo *gitalypb.Repository) error
git.Flag{Name: "--bare"},
git.Flag{Name: "--local"},
},
- Args: []string{repoPath, targetName},
+ Args: []string{repoPath, o.relativePath},
},
)
if err != nil {
diff --git a/internal/git/objectpool/pool.go b/internal/git/objectpool/pool.go
index ca4628d1b..1278b02c2 100644
--- a/internal/git/objectpool/pool.go
+++ b/internal/git/objectpool/pool.go
@@ -8,14 +8,25 @@ import (
"fmt"
"io"
"os"
- "path"
"path/filepath"
+ "regexp"
+ "strings"
"gitlab.com/gitlab-org/gitaly/internal/git"
"gitlab.com/gitlab-org/gitaly/internal/helper"
"gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
)
+// poolDirRegexp is used to validate object pool directory structure and name.
+var poolDirRegexp = regexp.MustCompile(`^@pools/([0-9a-f]{2})/([0-9a-f]{2})/([0-9a-f]{64})\.git$`)
+
+type errString string
+
+func (err errString) Error() string { return string(err) }
+
+// ErrInvalidPoolDir is returned when the object pool relative path is malformed.
+const ErrInvalidPoolDir errString = "invalid object pool directory"
+
// ObjectPool are a way to dedup objects between repositories, where the objects
// live in a pool in a distinct repository which is used as an alternate object
// store for other repositories.
@@ -27,13 +38,19 @@ type ObjectPool struct {
}
// NewObjectPool will initialize the object with the required data on the storage
-// shard. If the shard cannot be found, this function returns an error
+// shard. Relative path is validated to match the expected naming and directory
+// structure. If the shard cannot be found, this function returns an error.
func NewObjectPool(storageName, relativePath string) (pool *ObjectPool, err error) {
storagePath, err := helper.GetStorageByName(storageName)
if err != nil {
return nil, err
}
+ matches := poolDirRegexp.FindStringSubmatch(relativePath)
+ if matches == nil || !strings.HasPrefix(matches[3], matches[1]+matches[2]) {
+ return nil, ErrInvalidPoolDir
+ }
+
return &ObjectPool{storageName, storagePath, relativePath}, nil
}
@@ -72,10 +89,6 @@ func (o *ObjectPool) IsValid() bool {
// Create will create a pool for a repository and pull the required data to this
// pool. `repo` that is passed also joins the repository.
func (o *ObjectPool) Create(ctx context.Context, repo *gitalypb.Repository) (err error) {
- if err := os.MkdirAll(path.Dir(o.FullPath()), 0755); err != nil {
- return err
- }
-
if err := o.clone(ctx, repo); err != nil {
return fmt.Errorf("clone: %v", err)
}
diff --git a/internal/service/objectpool/create.go b/internal/service/objectpool/create.go
index 4affaa1c5..e0c377403 100644
--- a/internal/service/objectpool/create.go
+++ b/internal/service/objectpool/create.go
@@ -4,11 +4,14 @@ import (
"context"
"gitlab.com/gitlab-org/gitaly/internal/git/objectpool"
+ "gitlab.com/gitlab-org/gitaly/internal/helper"
"gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
+var errInvalidPoolDir = helper.ErrInvalidArgument(objectpool.ErrInvalidPoolDir)
+
func (s *server) CreateObjectPool(ctx context.Context, in *gitalypb.CreateObjectPoolRequest) (*gitalypb.CreateObjectPoolResponse, error) {
if in.GetOrigin() == nil {
return nil, status.Errorf(codes.InvalidArgument, "no origin repository")
@@ -55,5 +58,14 @@ func poolForRequest(req poolRequest) (*objectpool.ObjectPool, error) {
return nil, status.Errorf(codes.InvalidArgument, "no object pool repository")
}
- return objectpool.NewObjectPool(poolRepo.GetStorageName(), poolRepo.GetRelativePath())
+ pool, err := objectpool.NewObjectPool(poolRepo.GetStorageName(), poolRepo.GetRelativePath())
+ if err != nil {
+ if err == objectpool.ErrInvalidPoolDir {
+ return nil, errInvalidPoolDir
+ }
+
+ return nil, helper.ErrInternal(err)
+ }
+
+ return pool, nil
}
diff --git a/internal/service/objectpool/create_test.go b/internal/service/objectpool/create_test.go
index 8b6946fc9..f7a0e971c 100644
--- a/internal/service/objectpool/create_test.go
+++ b/internal/service/objectpool/create_test.go
@@ -3,6 +3,8 @@ package objectpool
import (
"os"
"path"
+ "path/filepath"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -73,7 +75,8 @@ func TestUnsuccessfulCreate(t *testing.T) {
testRepo, _, cleanupFn := testhelper.NewTestRepo(t)
defer cleanupFn()
- pool, err := objectpool.NewObjectPool("default", testhelper.NewTestObjectPoolName(t))
+ validPoolPath := testhelper.NewTestObjectPoolName(t)
+ pool, err := objectpool.NewObjectPool("default", validPoolPath)
require.NoError(t, err)
defer pool.Remove(ctx)
@@ -96,6 +99,45 @@ func TestUnsuccessfulCreate(t *testing.T) {
},
code: codes.InvalidArgument,
},
+ {
+ desc: "outside pools directory",
+ request: &gitalypb.CreateObjectPoolRequest{
+ Origin: testRepo,
+ ObjectPool: &gitalypb.ObjectPool{
+ Repository: &gitalypb.Repository{
+ StorageName: "default",
+ RelativePath: "outside-pools",
+ },
+ },
+ },
+ code: codes.InvalidArgument,
+ },
+ {
+ desc: "path must be lowercase",
+ request: &gitalypb.CreateObjectPoolRequest{
+ Origin: testRepo,
+ ObjectPool: &gitalypb.ObjectPool{
+ Repository: &gitalypb.Repository{
+ StorageName: "default",
+ RelativePath: strings.ToUpper(validPoolPath),
+ },
+ },
+ },
+ code: codes.InvalidArgument,
+ },
+ {
+ desc: "subdirectories must match first four pool digits",
+ request: &gitalypb.CreateObjectPoolRequest{
+ Origin: testRepo,
+ ObjectPool: &gitalypb.ObjectPool{
+ Repository: &gitalypb.Repository{
+ StorageName: "default",
+ RelativePath: "@pools/aa/bb/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff.git",
+ },
+ },
+ },
+ code: codes.InvalidArgument,
+ },
}
for _, tc := range testCases {
@@ -106,7 +148,7 @@ func TestUnsuccessfulCreate(t *testing.T) {
}
}
-func TestRemove(t *testing.T) {
+func TestDelete(t *testing.T) {
server, serverSocketPath := runObjectPoolServer(t)
defer server.Stop()
@@ -119,18 +161,63 @@ func TestRemove(t *testing.T) {
testRepo, _, cleanupFn := testhelper.NewTestRepo(t)
defer cleanupFn()
- pool, err := objectpool.NewObjectPool("default", testhelper.NewTestObjectPoolName(t))
+ validPoolPath := testhelper.NewTestObjectPoolName(t)
+ pool, err := objectpool.NewObjectPool("default", validPoolPath)
require.NoError(t, err)
require.NoError(t, pool.Create(ctx, testRepo))
- req := &gitalypb.DeleteObjectPoolRequest{
- ObjectPool: pool.ToProto(),
+ for _, tc := range []struct {
+ desc string
+ relativePath string
+ error error
+ }{
+ {
+ desc: "deleting outside pools directory fails",
+ relativePath: ".",
+ error: errInvalidPoolDir,
+ },
+ {
+ desc: "deleting pools directory fails",
+ relativePath: "@pools",
+ error: errInvalidPoolDir,
+ },
+ {
+ desc: "deleting first level subdirectory fails",
+ relativePath: "@pools/ab",
+ error: errInvalidPoolDir,
+ },
+ {
+ desc: "deleting second level subdirectory fails",
+ relativePath: "@pools/ab/cd",
+ error: errInvalidPoolDir,
+ },
+ {
+ desc: "deleting pool subdirectory fails",
+ relativePath: filepath.Join(validPoolPath, "objects"),
+ error: errInvalidPoolDir,
+ },
+ {
+ desc: "path traversing fails",
+ relativePath: validPoolPath + "/../../../../..",
+ error: errInvalidPoolDir,
+ },
+ {
+ desc: "deleting pool succeeds",
+ relativePath: validPoolPath,
+ },
+ {
+ desc: "deleting non-existent pool succeeds",
+ relativePath: validPoolPath,
+ },
+ } {
+ t.Run(tc.desc, func(t *testing.T) {
+ _, err := client.DeleteObjectPool(ctx, &gitalypb.DeleteObjectPoolRequest{ObjectPool: &gitalypb.ObjectPool{
+ Repository: &gitalypb.Repository{
+ StorageName: "default",
+ RelativePath: tc.relativePath,
+ },
+ }})
+ require.Equal(t, tc.error, err)
+ })
}
-
- _, err = client.DeleteObjectPool(ctx, req)
- require.NoError(t, err)
-
- // Removing again should be possible
- _, err = client.DeleteObjectPool(ctx, req)
- require.NoError(t, err)
}
diff --git a/internal/service/objectpool/link_test.go b/internal/service/objectpool/link_test.go
index 9265eaf08..3eafd62dd 100644
--- a/internal/service/objectpool/link_test.go
+++ b/internal/service/objectpool/link_test.go
@@ -276,7 +276,7 @@ func TestUnlink(t *testing.T) {
ObjectPool: &gitalypb.ObjectPool{
Repository: &gitalypb.Repository{
StorageName: testRepo.GetStorageName(),
- RelativePath: "pool/does/not/exist",
+ RelativePath: testhelper.NewTestObjectPoolName(t), // does not exist
},
},
},
diff --git a/internal/service/repository/clone_from_pool_internal_test.go b/internal/service/repository/clone_from_pool_internal_test.go
index b31b8c428..c32453ee9 100644
--- a/internal/service/repository/clone_from_pool_internal_test.go
+++ b/internal/service/repository/clone_from_pool_internal_test.go
@@ -17,7 +17,9 @@ import (
)
func NewTestObjectPool(t *testing.T) (*objectpool.ObjectPool, *gitalypb.Repository) {
- repo, _, relativePath := testhelper.CreateRepo(t, testhelper.GitlabTestStoragePath())
+ storagePath := testhelper.GitlabTestStoragePath()
+ relativePath := testhelper.NewTestObjectPoolName(t)
+ repo := testhelper.CreateRepo(t, storagePath, relativePath)
pool, err := objectpool.NewObjectPool(repo.GetStorageName(), relativePath)
require.NoError(t, err)
diff --git a/internal/testhelper/testhelper.go b/internal/testhelper/testhelper.go
index 5d7d0647a..72fff1ddb 100644
--- a/internal/testhelper/testhelper.go
+++ b/internal/testhelper/testhelper.go
@@ -3,9 +3,9 @@ package testhelper
import (
"bytes"
"context"
+ "crypto/sha256"
"encoding/base64"
"encoding/json"
- "errors"
"fmt"
"io"
"io/ioutil"
@@ -386,30 +386,14 @@ func Context() (context.Context, func()) {
return context.WithCancel(context.Background())
}
-func hashedRepoPath(dirName string) (string, error) {
- if len(dirName) < 4 {
- return "", errors.New("dirname is too short")
- }
-
- return filepath.Join(dirName[0:2], dirName[2:4], dirName[4:]), nil
-}
-
// CreateRepo creates a temporary directory for a repo, without initializing it
-func CreateRepo(t testing.TB, storagePath string) (repo *gitalypb.Repository, repoPath, relativePath string) {
- random, err := text.RandomHex(20)
- require.NoError(t, err)
-
- hashedPath, err := hashedRepoPath(random)
- require.NoError(t, err)
-
- repoPath = filepath.Join(storagePath, hashedPath)
- require.NoError(t, os.MkdirAll(filepath.Dir(repoPath), 0755), "making repo parent dir")
-
- relativePath, err = filepath.Rel(storagePath, repoPath)
- require.NoError(t, err)
- repo = &gitalypb.Repository{StorageName: "default", RelativePath: relativePath, GlRepository: "project-1"}
-
- return repo, repoPath, relativePath
+func CreateRepo(t testing.TB, storagePath, relativePath string) *gitalypb.Repository {
+ require.NoError(t, os.MkdirAll(filepath.Dir(storagePath), 0755), "making repo parent dir")
+ return &gitalypb.Repository{
+ StorageName: "default",
+ RelativePath: relativePath,
+ GlRepository: "project-1",
+ }
}
// InitBareRepo creates a new bare repository
@@ -423,7 +407,10 @@ func InitRepoWithWorktree(t *testing.T) (*gitalypb.Repository, string, func()) {
}
func initRepo(t *testing.T, bare bool) (*gitalypb.Repository, string, func()) {
- repo, repoPath, _ := CreateRepo(t, GitlabTestStoragePath())
+ storagePath := GitlabTestStoragePath()
+ relativePath := NewRepositoryName(t)
+ repoPath := filepath.Join(storagePath, relativePath)
+
args := []string{"init"}
if bare {
args = append(args, "--bare")
@@ -431,6 +418,7 @@ func initRepo(t *testing.T, bare bool) (*gitalypb.Repository, string, func()) {
MustRunCommand(t, nil, "git", append(args, repoPath)...)
+ repo := CreateRepo(t, storagePath, relativePath)
if !bare {
repo.RelativePath = path.Join(repo.RelativePath, ".git")
}
@@ -451,7 +439,10 @@ func NewTestRepoWithWorktree(t testing.TB) (repo *gitalypb.Repository, repoPath
func cloneTestRepo(t testing.TB, bare bool) (repo *gitalypb.Repository, repoPath string, cleanup func()) {
storagePath := GitlabTestStoragePath()
- repo, repoPath, relativePath := CreateRepo(t, storagePath)
+ relativePath := NewRepositoryName(t)
+ repoPath = filepath.Join(storagePath, relativePath)
+
+ repo = CreateRepo(t, storagePath, relativePath)
testRepo := TestRepository()
testRepoPath := path.Join(storagePath, testRepo.RelativePath)
args := []string{"clone", "--no-hardlinks", "--dissociate"}
@@ -510,12 +501,27 @@ func AssertPathNotExists(t *testing.T, path string) {
assert.True(t, os.IsNotExist(err), "file should not exist: %s", path)
}
-// NewTestObjectPoolName returns a random pool repository name.
-func NewTestObjectPoolName(t *testing.T) string {
- b, err := text.RandomHex(5)
+// newDiskHash generates a random directory path following the Rails app's
+// approach in the hashed storage module, formatted as '[0-9a-f]{2}/[0-9a-f]{2}/[0-9a-f]{64}'.
+// https://gitlab.com/gitlab-org/gitlab/-/blob/f5c7d8eb1dd4eee5106123e04dec26d277ff6a83/app/models/storage/hashed.rb#L38-43
+func newDiskHash(t testing.TB) string {
+ // rails app calculates a sha256 and uses its hex representation
+ // as the directory path
+ b, err := text.RandomHex(sha256.Size)
require.NoError(t, err)
+ return filepath.Join(b[0:2], b[2:4], fmt.Sprintf("%s.git", b))
+}
+
+// NewRepositoryName returns a random repository hash
+// in format '@hashed/[0-9a-f]{2}/[0-9a-f]{2}/[0-9a-f]{64}.git'.
+func NewRepositoryName(t testing.TB) string {
+ return filepath.Join("@hashed", newDiskHash(t))
+}
- return fmt.Sprintf("pool-%s.git", b)
+// NewTestObjectPoolName returns a random pool repository name
+// in format '@pools/[0-9a-z]{2}/[0-9a-z]{2}/[0-9a-z]{64}.git'.
+func NewTestObjectPoolName(t testing.TB) string {
+ return filepath.Join("@pools", newDiskHash(t))
}
// CreateLooseRef creates a ref that points to master