mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-11 17:12:51 +00:00
Merge pull request #700 from monopole/restrictLoading
Restrict loading to root or below.
This commit is contained in:
@@ -26,54 +26,56 @@ import (
|
|||||||
"sigs.k8s.io/kustomize/pkg/ifc"
|
"sigs.k8s.io/kustomize/pkg/ifc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// fileLoader loads files, returning an array of bytes.
|
// fileLoader is a kustomization's interface to files.
|
||||||
// It also enforces two kustomization requirements:
|
|
||||||
//
|
//
|
||||||
// 1) relocatable
|
// The directory in which a kustomization file sits
|
||||||
|
// is referred to below as the kustomization's root.
|
||||||
//
|
//
|
||||||
// A kustomization and the resources, bases,
|
// An instance of fileLoader has an immutable root,
|
||||||
// patches, etc. that it depends on should be
|
// and offers a `New` method returning a new loader
|
||||||
// relocatable, so all path specifications
|
// with a new root.
|
||||||
// must be relative, not absolute. The paths
|
|
||||||
// are taken relative to the location of the
|
|
||||||
// kusttomization file.
|
|
||||||
//
|
//
|
||||||
// 2) acyclic
|
// A kustomization file refers to two kinds of files:
|
||||||
//
|
//
|
||||||
// There should be no cycles in overlay to base
|
// * supplemental data paths
|
||||||
// relationships, including no cycles between
|
|
||||||
// git repositories.
|
|
||||||
//
|
//
|
||||||
// The loader has a notion of a current working directory
|
// `Load` is used to visit these paths.
|
||||||
// (CWD), called 'root', that is independent of the CWD
|
|
||||||
// of the process. When `Load` is called with a file path
|
|
||||||
// argument, the load is done relative to this root,
|
|
||||||
// not relative to the process CWD.
|
|
||||||
//
|
//
|
||||||
// The loader offers a `New` method returning a new loader
|
// They must terminate in or below the root.
|
||||||
// with a new root. The new root can be one of two things,
|
|
||||||
// a remote git repo URL, or a directory specified relative
|
|
||||||
// to the current root. In the former case, the remote
|
|
||||||
// repository is locally cloned, and the new loader is
|
|
||||||
// rooted on a path in that clone.
|
|
||||||
//
|
//
|
||||||
// Crucially, a root history is used to so that New fails
|
// They hold things like resources, patches,
|
||||||
// if its argument either matches or is a parent of the
|
// data for ConfigMaps, etc.
|
||||||
// current or any previously used root.
|
|
||||||
//
|
//
|
||||||
// This disallows:
|
// * bases; other kustomizations
|
||||||
//
|
//
|
||||||
// * A base that is a git repository that, in turn,
|
// `New` is used to load bases.
|
||||||
// specifies a base repository seen previously
|
|
||||||
// in the loading process (a cycle).
|
|
||||||
//
|
//
|
||||||
// * An overlay depending on a base positioned at or
|
// A base can be either a remote git repo URL, or
|
||||||
// above it. I.e. '../foo' is OK, but '.', '..',
|
// a directory specified relative to the current
|
||||||
// '../..', etc. are disallowed. Allowing such a
|
// root. In the former case, the repo is locally
|
||||||
// base has no advantages and encourage cycles,
|
// cloned, and the new loader is rooted on a path
|
||||||
// particularly if some future change were to
|
// in that clone.
|
||||||
// introduce globbing to file specifications in
|
//
|
||||||
// the kustomization file.
|
// As loaders create new loaders, a root history
|
||||||
|
// is established, and used to disallow:
|
||||||
|
//
|
||||||
|
// - A base that is a repository that, in turn,
|
||||||
|
// specifies a base repository seen previously
|
||||||
|
// in the loading stack (a cycle).
|
||||||
|
//
|
||||||
|
// - An overlay depending on a base positioned at
|
||||||
|
// or above it. I.e. '../foo' is OK, but '.',
|
||||||
|
// '..', '../..', etc. are disallowed. Allowing
|
||||||
|
// such a base has no advantages and encourages
|
||||||
|
// cycles, particularly if some future change
|
||||||
|
// were to introduce globbing to file
|
||||||
|
// specifications in the kustomization file.
|
||||||
|
//
|
||||||
|
// These restrictions assure that kustomizations
|
||||||
|
// are self-contained and relocatable, and impose
|
||||||
|
// some safety when relying on remote kustomizations,
|
||||||
|
// e.g. a ConfigMap generator specified to read
|
||||||
|
// from /etc/passwd will fail.
|
||||||
//
|
//
|
||||||
type fileLoader struct {
|
type fileLoader struct {
|
||||||
// Previously visited absolute directory paths.
|
// Previously visited absolute directory paths.
|
||||||
@@ -121,7 +123,7 @@ func newFileLoaderAt(
|
|||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"loader root cannot be empty")
|
"loader root cannot be empty")
|
||||||
}
|
}
|
||||||
absRoot, err := filepath.Abs(root)
|
absRoot, err := cleanedAbs(root, fSys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"absolute path error in '%s' : %v", root, err)
|
"absolute path error in '%s' : %v", root, err)
|
||||||
@@ -141,6 +143,22 @@ func newFileLoaderAt(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanedAbs returns a cleaned, absolute path
|
||||||
|
// with no symbolic links.
|
||||||
|
func cleanedAbs(path string, fSys fs.FileSystem) (string, error) {
|
||||||
|
absRoot, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"abs path error on '%s' : %v", path, err)
|
||||||
|
}
|
||||||
|
deLinked, err := fSys.EvalSymlinks(absRoot)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"evalsymlink failure on '%s' : %v", path, err)
|
||||||
|
}
|
||||||
|
return deLinked, nil
|
||||||
|
}
|
||||||
|
|
||||||
// New returns a new Loader, rooted relative to current loader,
|
// New returns a new Loader, rooted relative to current loader,
|
||||||
// or rooted in a temp directory holding a git repo clone.
|
// or rooted in a temp directory holding a git repo clone.
|
||||||
func (l *fileLoader) New(path string) (ifc.Loader, error) {
|
func (l *fileLoader) New(path string) (ifc.Loader, error) {
|
||||||
@@ -158,12 +176,8 @@ func (l *fileLoader) New(path string) (ifc.Loader, error) {
|
|||||||
if filepath.IsAbs(path) {
|
if filepath.IsAbs(path) {
|
||||||
return nil, fmt.Errorf("new root '%s' cannot be absolute", path)
|
return nil, fmt.Errorf("new root '%s' cannot be absolute", path)
|
||||||
}
|
}
|
||||||
absRoot, err := filepath.Abs(filepath.Join(l.Root(), path))
|
return newFileLoaderAt(
|
||||||
if err != nil {
|
filepath.Join(l.Root(), path), l.fSys, l.roots, l.cloner)
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"problem joining '%s' to '%s': %v", l.Root(), path, err)
|
|
||||||
}
|
|
||||||
return newFileLoaderAt(absRoot, l.fSys, l.roots, l.cloner)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newGitLoader returns a new Loader pinned to a temporary
|
// newGitLoader returns a new Loader pinned to a temporary
|
||||||
@@ -205,13 +219,52 @@ func isPathEqualToOrAbove(path string, roots []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load returns content of file at the given relative path.
|
// Load returns content of file at the given relative path,
|
||||||
|
// else an error. The path must refer to a file in or
|
||||||
|
// below the current Root().
|
||||||
func (l *fileLoader) Load(path string) ([]byte, error) {
|
func (l *fileLoader) Load(path string) ([]byte, error) {
|
||||||
if filepath.IsAbs(path) {
|
if filepath.IsAbs(path) {
|
||||||
return nil, fmt.Errorf(
|
return nil, l.loadOutOfBounds(path)
|
||||||
"must use relative path; '%s' is absolute", path)
|
|
||||||
}
|
}
|
||||||
return l.fSys.ReadFile(filepath.Join(l.Root(), path))
|
path, err := cleanedAbs(
|
||||||
|
filepath.Join(l.Root(), path), l.fSys)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !l.isInOrBelowRoot(path) {
|
||||||
|
return nil, l.loadOutOfBounds(path)
|
||||||
|
}
|
||||||
|
return l.fSys.ReadFile(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isInOrBelowRoot is true if the argument is in or
|
||||||
|
// below Root() from purely a path perspective (no
|
||||||
|
// check for actual file existence). For this to work,
|
||||||
|
// both the given argument "path" and l.Root() must
|
||||||
|
// be cleaned, absolute paths, and l.Root() must be
|
||||||
|
// a directory. Both conditions enforced elsewhere.
|
||||||
|
//
|
||||||
|
// This is tested on linux, but will have trouble
|
||||||
|
// on other operating systems. As soon as related
|
||||||
|
// work is completed in the core filepath package,
|
||||||
|
// this code should be refactored to use it.
|
||||||
|
// See:
|
||||||
|
// https://github.com/golang/go/issues/18355
|
||||||
|
// https://github.com/golang/dep/issues/296
|
||||||
|
// https://github.com/golang/dep/blob/master/internal/fs/fs.go#L33
|
||||||
|
// https://codereview.appspot.com/5712045
|
||||||
|
func (l *fileLoader) isInOrBelowRoot(path string) bool {
|
||||||
|
if l.Root() == string(filepath.Separator) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(
|
||||||
|
path, l.Root()+string(filepath.Separator))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *fileLoader) loadOutOfBounds(path string) error {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"security; file '%s' is not in or below '%s'",
|
||||||
|
path, l.Root())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup runs the cleaner.
|
// Cleanup runs the cleaner.
|
||||||
|
|||||||
@@ -17,11 +17,16 @@ limitations under the License.
|
|||||||
package loader
|
package loader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sigs.k8s.io/kustomize/pkg/fs"
|
"sigs.k8s.io/kustomize/pkg/fs"
|
||||||
|
"sigs.k8s.io/kustomize/pkg/ifc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testData struct {
|
type testData struct {
|
||||||
@@ -198,3 +203,102 @@ func TestLoaderMisc(t *testing.T) {
|
|||||||
t.Fatalf("Expected error")
|
t.Fatalf("Expected error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRestrictedLoadingInRealLoader(t *testing.T) {
|
||||||
|
// Create a structure like this
|
||||||
|
//
|
||||||
|
// /tmp/kustomize-test-SZwCZYjySj
|
||||||
|
// ├── base
|
||||||
|
// │ ├── okayData
|
||||||
|
// │ ├── symLinkToOkayData -> okayData
|
||||||
|
// │ └── symLinkToForbiddenData -> ../forbiddenData
|
||||||
|
// └── forbiddenData
|
||||||
|
//
|
||||||
|
dir, err := ioutil.TempDir("", "kustomize-test-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
fSys := fs.MakeRealFS()
|
||||||
|
fSys.Mkdir(filepath.Join(dir, "base"))
|
||||||
|
|
||||||
|
contentOk := "hi there, i'm OK data"
|
||||||
|
fSys.WriteFile(
|
||||||
|
filepath.Join(dir, "base", "okayData"), []byte(contentOk))
|
||||||
|
|
||||||
|
contentForbidden := "don't be looking at me"
|
||||||
|
fSys.WriteFile(
|
||||||
|
filepath.Join(dir, "forbiddenData"), []byte(contentForbidden))
|
||||||
|
|
||||||
|
os.Symlink(
|
||||||
|
filepath.Join(dir, "base", "okayData"),
|
||||||
|
filepath.Join(dir, "base", "symLinkToOkayData"))
|
||||||
|
os.Symlink(
|
||||||
|
filepath.Join(dir, "forbiddenData"),
|
||||||
|
filepath.Join(dir, "base", "symLinkToForbiddenData"))
|
||||||
|
|
||||||
|
var l ifc.Loader
|
||||||
|
|
||||||
|
l = newLoaderOrDie(fSys, dir)
|
||||||
|
|
||||||
|
// Sanity checks - loading from perspective of "dir".
|
||||||
|
// Everything works, including reading from a subdirectory.
|
||||||
|
data, err := l.Load(path.Join("base", "okayData"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != contentOk {
|
||||||
|
t.Fatalf("unexpected content: %v", data)
|
||||||
|
}
|
||||||
|
data, err = l.Load("forbiddenData")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != contentForbidden {
|
||||||
|
t.Fatalf("unexpected content: %v", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop in.
|
||||||
|
l, err = l.New("base")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reading okayData works.
|
||||||
|
data, err = l.Load("okayData")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != contentOk {
|
||||||
|
t.Fatalf("unexpected content: %v", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reading local symlink to okayData works.
|
||||||
|
data, err = l.Load("symLinkToOkayData")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != contentOk {
|
||||||
|
t.Fatalf("unexpected content: %v", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reading symlink to forbiddenData fails.
|
||||||
|
_, err = l.Load("symLinkToForbiddenData")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "is not in or below") {
|
||||||
|
t.Fatalf("unexpected err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to read "up" fails, though earlier we were
|
||||||
|
// able to read this file when root was "..".
|
||||||
|
_, err = l.Load("../forbiddenData")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "is not in or below") {
|
||||||
|
t.Fatalf("unexpected err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user