Remove local load restrictions.

This commit is contained in:
Jeffrey Regan
2019-04-18 19:25:06 -07:00
parent 1b1f91580e
commit 3c58c9d132
6 changed files with 290 additions and 86 deletions

View File

@@ -45,7 +45,8 @@ import (
// These paths refer to resources, patches, // These paths refer to resources, patches,
// data for ConfigMaps and Secrets, etc. // data for ConfigMaps and Secrets, etc.
// //
// They must terminate in or below the root. // The loadRestrictor may disallow certain paths
// or classes of paths.
// //
// * bases (other kustomizations) // * bases (other kustomizations)
// //
@@ -75,8 +76,8 @@ import (
// These restrictions assure that kustomizations // These restrictions assure that kustomizations
// are self-contained and relocatable, and impose // are self-contained and relocatable, and impose
// some safety when relying on remote kustomizations, // some safety when relying on remote kustomizations,
// e.g. a ConfigMap generator specified to read // e.g. a remotely loaded ConfigMap generator specified
// from /etc/passwd will fail. // to read from /etc/passwd will fail.
// //
type fileLoader struct { type fileLoader struct {
// Loader that spawned this loader. // Loader that spawned this loader.
@@ -88,6 +89,9 @@ type fileLoader struct {
// paths relative to this directory. // paths relative to this directory.
root fs.ConfirmedDir root fs.ConfirmedDir
// Restricts behavior of Load function.
loadRestrictor LoadRestrictorFunc
// If this is non-nil, the files were // If this is non-nil, the files were
// obtained from the given repository. // obtained from the given repository.
repoSpec *git.RepoSpec repoSpec *git.RepoSpec
@@ -105,13 +109,17 @@ type fileLoader struct {
const CWD = "." const CWD = "."
// NewFileLoaderAtCwd returns a loader that loads from ".". // NewFileLoaderAtCwd returns a loader that loads from ".".
// A convenience for kustomize edit commands.
func NewFileLoaderAtCwd(fSys fs.FileSystem) *fileLoader { func NewFileLoaderAtCwd(fSys fs.FileSystem) *fileLoader {
return newLoaderOrDie(fSys, CWD) return newLoaderOrDie(
RestrictionRootOnly, fSys, CWD)
} }
// NewFileLoaderAtRoot returns a loader that loads from "/". // NewFileLoaderAtRoot returns a loader that loads from "/".
// A convenience for tests.
func NewFileLoaderAtRoot(fSys fs.FileSystem) *fileLoader { func NewFileLoaderAtRoot(fSys fs.FileSystem) *fileLoader {
return newLoaderOrDie(fSys, string(filepath.Separator)) return newLoaderOrDie(
RestrictionRootOnly, fSys, string(filepath.Separator))
} }
// Root returns the absolute path that is prepended to any // Root returns the absolute path that is prepended to any
@@ -120,20 +128,23 @@ func (l *fileLoader) Root() string {
return l.root.String() return l.root.String()
} }
func newLoaderOrDie(fSys fs.FileSystem, path string) *fileLoader { func newLoaderOrDie(
lr LoadRestrictorFunc, fSys fs.FileSystem, path string) *fileLoader {
root, err := demandDirectoryRoot(fSys, path) root, err := demandDirectoryRoot(fSys, path)
if err != nil { if err != nil {
log.Fatalf("unable to make loader at '%s'; %v", path, err) log.Fatalf("unable to make loader at '%s'; %v", path, err)
} }
return newLoaderAtConfirmedDir( return newLoaderAtConfirmedDir(
root, fSys, nil, git.ClonerUsingGitExec) lr, root, fSys, nil, git.ClonerUsingGitExec)
} }
// newLoaderAtConfirmedDir returns a new fileLoader with given root. // newLoaderAtConfirmedDir returns a new fileLoader with given root.
func newLoaderAtConfirmedDir( func newLoaderAtConfirmedDir(
lr LoadRestrictorFunc,
root fs.ConfirmedDir, fSys fs.FileSystem, root fs.ConfirmedDir, fSys fs.FileSystem,
referrer *fileLoader, cloner git.Cloner) *fileLoader { referrer *fileLoader, cloner git.Cloner) *fileLoader {
return &fileLoader{ return &fileLoader{
loadRestrictor: lr,
root: root, root: root,
referrer: referrer, referrer: referrer,
fSys: fSys, fSys: fSys,
@@ -190,7 +201,7 @@ func (l *fileLoader) New(path string) (ifc.Loader, error) {
return nil, err return nil, err
} }
return newLoaderAtConfirmedDir( return newLoaderAtConfirmedDir(
root, l.fSys, l, l.cloner), nil l.loadRestrictor, root, l.fSys, l, l.cloner), nil
} }
// newLoaderAtGitClone returns a new Loader pinned to a temporary // newLoaderAtGitClone returns a new Loader pinned to a temporary
@@ -216,6 +227,8 @@ func newLoaderAtGitClone(
repoSpec.AbsPath(), f) repoSpec.AbsPath(), f)
} }
return &fileLoader{ return &fileLoader{
// Clones never allowed to escape root.
loadRestrictor: RestrictionRootOnly,
root: root, root: root,
referrer: referrer, referrer: referrer,
repoSpec: repoSpec, repoSpec: repoSpec,
@@ -288,29 +301,16 @@ func (l *fileLoader) errIfRepoCycle(newRepoSpec *git.RepoSpec) error {
// Load returns the content of file at the given path, // Load returns the content of file at the given path,
// else an error. Relative paths are taken relative // else an error. Relative paths are taken relative
// relative to the root. // to the 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, l.loadOutOfBounds(path) path = l.root.Join(path)
} }
d, f, err := l.fSys.CleanedAbs(l.root.Join(path)) path, err := l.loadRestrictor(l.fSys, l.root, path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if f == "" { return l.fSys.ReadFile(path)
return nil, fmt.Errorf(
"'%s' must be a file (got d='%s')", path, d)
}
if !d.HasPrefix(l.root) {
return nil, l.loadOutOfBounds(path)
}
return l.fSys.ReadFile(d.Join(f))
}
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.

View File

@@ -25,11 +25,10 @@ import (
"strings" "strings"
"testing" "testing"
"sigs.k8s.io/kustomize/pkg/git"
"sigs.k8s.io/kustomize/pkg/pgmconfig"
"sigs.k8s.io/kustomize/pkg/fs" "sigs.k8s.io/kustomize/pkg/fs"
"sigs.k8s.io/kustomize/pkg/git"
"sigs.k8s.io/kustomize/pkg/ifc" "sigs.k8s.io/kustomize/pkg/ifc"
"sigs.k8s.io/kustomize/pkg/pgmconfig"
) )
type testData struct { type testData struct {
@@ -207,46 +206,47 @@ func TestLoaderMisc(t *testing.T) {
} }
} }
func TestRestrictedLoadingInRealLoader(t *testing.T) { const (
contentOk = "hi there, i'm OK data"
contentExteriorData = "i am data from outside the root"
)
// Create a structure like this // Create a structure like this
// //
// /tmp/kustomize-test-SZwCZYjySj // /tmp/kustomize-test-random
// ├── base // ├── base
// │ ├── okayData // │ ├── okayData
// │ ├── symLinkToOkayData -> okayData // │ ├── symLinkToOkayData -> okayData
// │ └── symLinkToForbiddenData -> ../forbiddenData // │ └── symLinkToExteriorData -> ../exteriorData
// └── forbiddenData // └── exteriorData
// //
func commonSetupForLoaderRestrictionTest() (string, fs.FileSystem, error) {
dir, err := ioutil.TempDir("", "kustomize-test-") dir, err := ioutil.TempDir("", "kustomize-test-")
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) return "", nil, err
} }
defer os.RemoveAll(dir)
fSys := fs.MakeRealFS() fSys := fs.MakeRealFS()
fSys.Mkdir(filepath.Join(dir, "base")) fSys.Mkdir(filepath.Join(dir, "base"))
contentOk := "hi there, i'm OK data"
fSys.WriteFile( fSys.WriteFile(
filepath.Join(dir, "base", "okayData"), []byte(contentOk)) filepath.Join(dir, "base", "okayData"), []byte(contentOk))
contentForbidden := "don't be looking at me"
fSys.WriteFile( fSys.WriteFile(
filepath.Join(dir, "forbiddenData"), []byte(contentForbidden)) filepath.Join(dir, "exteriorData"), []byte(contentExteriorData))
os.Symlink( os.Symlink(
filepath.Join(dir, "base", "okayData"), filepath.Join(dir, "base", "okayData"),
filepath.Join(dir, "base", "symLinkToOkayData")) filepath.Join(dir, "base", "symLinkToOkayData"))
os.Symlink( os.Symlink(
filepath.Join(dir, "forbiddenData"), filepath.Join(dir, "exteriorData"),
filepath.Join(dir, "base", "symLinkToForbiddenData")) filepath.Join(dir, "base", "symLinkToExteriorData"))
return dir, fSys, nil
}
var l ifc.Loader // Make sure everything works when loading files
// in or below the loader root.
l = newLoaderOrDie(fSys, dir) func doSanityChecksAndDropIntoBase(
t *testing.T, l ifc.Loader) ifc.Loader {
// Sanity checks - loading from perspective of "dir".
// Everything works, including reading from a subdirectory.
data, err := l.Load(path.Join("base", "okayData")) data, err := l.Load(path.Join("base", "okayData"))
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
@@ -254,11 +254,11 @@ func TestRestrictedLoadingInRealLoader(t *testing.T) {
if string(data) != contentOk { if string(data) != contentOk {
t.Fatalf("unexpected content: %v", data) t.Fatalf("unexpected content: %v", data)
} }
data, err = l.Load("forbiddenData") data, err = l.Load("exteriorData")
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
if string(data) != contentForbidden { if string(data) != contentExteriorData {
t.Fatalf("unexpected content: %v", data) t.Fatalf("unexpected content: %v", data)
} }
@@ -285,9 +285,24 @@ func TestRestrictedLoadingInRealLoader(t *testing.T) {
if string(data) != contentOk { if string(data) != contentOk {
t.Fatalf("unexpected content: %v", data) t.Fatalf("unexpected content: %v", data)
} }
return l
}
// Reading symlink to forbiddenData fails. func TestRestrictionRootInRealLoader(t *testing.T) {
_, err = l.Load("symLinkToForbiddenData") dir, fSys, err := commonSetupForLoaderRestrictionTest()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer os.RemoveAll(dir)
var l ifc.Loader
l = newLoaderOrDie(RestrictionRootOnly, fSys, dir)
l = doSanityChecksAndDropIntoBase(t, l)
// Reading symlink to exteriorData fails.
_, err = l.Load("symLinkToExteriorData")
if err == nil { if err == nil {
t.Fatalf("expected error") t.Fatalf("expected error")
} }
@@ -297,7 +312,7 @@ func TestRestrictedLoadingInRealLoader(t *testing.T) {
// Attempt to read "up" fails, though earlier we were // Attempt to read "up" fails, though earlier we were
// able to read this file when root was "..". // able to read this file when root was "..".
_, err = l.Load("../forbiddenData") _, err = l.Load("../exteriorData")
if err == nil { if err == nil {
t.Fatalf("expected error") t.Fatalf("expected error")
} }
@@ -306,6 +321,32 @@ func TestRestrictedLoadingInRealLoader(t *testing.T) {
} }
} }
func TestRestrictionNoneInRealLoader(t *testing.T) {
dir, fSys, err := commonSetupForLoaderRestrictionTest()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer os.RemoveAll(dir)
var l ifc.Loader
l = newLoaderOrDie(RestrictionNone, fSys, dir)
l = doSanityChecksAndDropIntoBase(t, l)
// Reading symlink to exteriorData works.
_, err = l.Load("symLinkToExteriorData")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Attempt to read "up" works.
_, err = l.Load("../exteriorData")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func splitOnNthSlash(v string, n int) (string, string) { func splitOnNthSlash(v string, n int) (string, string) {
left := "" left := ""
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
@@ -393,7 +434,7 @@ func TestLoaderDisallowsLocalBaseFromRemoteOverlay(t *testing.T) {
// Establish that a local overlay can navigate // Establish that a local overlay can navigate
// to the local bases. // to the local bases.
l1 = newLoaderOrDie(fSys, cloneRoot+"/foo/overlay") l1 = newLoaderOrDie(RestrictionRootOnly, fSys, cloneRoot+"/foo/overlay")
if l1.Root() != cloneRoot+"/foo/overlay" { if l1.Root() != cloneRoot+"/foo/overlay" {
t.Fatalf("unexpected root %s", l1.Root()) t.Fatalf("unexpected root %s", l1.Root())
} }
@@ -468,7 +509,7 @@ func TestLocalLoaderReferencingGitBase(t *testing.T) {
t.Fatalf("unexpected err: %v\n", err) t.Fatalf("unexpected err: %v\n", err)
} }
l1 := newLoaderAtConfirmedDir( l1 := newLoaderAtConfirmedDir(
root, fSys, nil, RestrictionRootOnly, root, fSys, nil,
git.DoNothingCloner(fs.ConfirmedDir(cloneRoot))) git.DoNothingCloner(fs.ConfirmedDir(cloneRoot)))
if l1.Root() != topDir { if l1.Root() != topDir {
t.Fatalf("unexpected root %s", l1.Root()) t.Fatalf("unexpected root %s", l1.Root())

View File

@@ -23,17 +23,24 @@ import (
"sigs.k8s.io/kustomize/pkg/ifc" "sigs.k8s.io/kustomize/pkg/ifc"
) )
// NewLoader returns a Loader. // NewLoader returns a Loader pointed at the given target.
func NewLoader(path string, fSys fs.FileSystem) (ifc.Loader, error) { // If the target is remote, the loader will be restricted
repoSpec, err := git.NewRepoSpecFromUrl(path) // to the root and below only. If the target is local, the
// loader will have no restrictions. If the local target
// attempts to transitively load remote bases, they will all
// be root-only restricted.
func NewLoader(
target string, fSys fs.FileSystem) (ifc.Loader, error) {
repoSpec, err := git.NewRepoSpecFromUrl(target)
if err == nil { if err == nil {
// The target qualifies as a remote git target.
return newLoaderAtGitClone( return newLoaderAtGitClone(
repoSpec, fSys, nil, git.ClonerUsingGitExec) repoSpec, fSys, nil, git.ClonerUsingGitExec)
} }
root, err := demandDirectoryRoot(fSys, path) root, err := demandDirectoryRoot(fSys, target)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newLoaderAtConfirmedDir( return newLoaderAtConfirmedDir(
root, fSys, nil, git.ClonerUsingGitExec), nil RestrictionNone, root, fSys, nil, git.ClonerUsingGitExec), nil
} }

View File

@@ -0,0 +1,48 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package loader
import (
"fmt"
"sigs.k8s.io/kustomize/pkg/fs"
)
type LoadRestrictorFunc func(
fs.FileSystem, fs.ConfirmedDir, string) (string, error)
func RestrictionRootOnly(
fSys fs.FileSystem, root fs.ConfirmedDir, path string) (string, error) {
d, f, err := fSys.CleanedAbs(path)
if err != nil {
return "", err
}
if f == "" {
return "", fmt.Errorf("'%s' must be a file", path)
}
if !d.HasPrefix(root) {
return "", fmt.Errorf(
"security; file '%s' is not in or below '%s'",
path, root)
}
return d.Join(f), nil
}
func RestrictionNone(
_ fs.FileSystem, _ fs.ConfirmedDir, path string) (string, error) {
return path, nil
}

View File

@@ -0,0 +1,74 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package loader
import (
"strings"
"testing"
"sigs.k8s.io/kustomize/pkg/fs"
)
func TestRestrictionNone(t *testing.T) {
fSys := fs.MakeFakeFS()
root := fs.ConfirmedDir("irrelevant")
path := "whatever"
p, err := RestrictionNone(fSys, root, path)
if err != nil {
t.Fatal(err)
}
if p != path {
t.Fatalf("expected '%s', got '%s'", path, p)
}
}
func TestRestrictionRootOnly(t *testing.T) {
fSys := fs.MakeFakeFS()
root := fs.ConfirmedDir("/tmp/foo")
path := "/tmp/foo/whatever/beans"
p, err := RestrictionRootOnly(fSys, root, path)
if err != nil {
t.Fatal(err)
}
if p != path {
t.Fatalf("expected '%s', got '%s'", path, p)
}
// Legal.
path = "/tmp/foo/whatever/../../foo/whatever"
p, err = RestrictionRootOnly(fSys, root, path)
if err != nil {
t.Fatal(err)
}
path = "/tmp/foo/whatever"
if p != path {
t.Fatalf("expected '%s', got '%s'", path, p)
}
// Illegal.
path = "/tmp/illegal"
_, err = RestrictionRootOnly(fSys, root, path)
if err == nil {
t.Fatal("should have an error")
}
if !strings.Contains(
err.Error(),
"file '/tmp/illegal' is not in or below '/tmp/foo'") {
t.Fatalf("unexpected err: %s", err)
}
}

View File

@@ -17,7 +17,6 @@ limitations under the License.
package target_test package target_test
import ( import (
"strings"
"testing" "testing"
) )
@@ -182,7 +181,7 @@ spec:
`) `)
} }
func TestSharedPatchDisAllowed(t *testing.T) { func TestSharedPatchAllowed(t *testing.T) {
th := NewKustTestHarness(t, "/app/overlay") th := NewKustTestHarness(t, "/app/overlay")
writeSmallBase(th) writeSmallBase(th)
th.writeK("/app/overlay", ` th.writeK("/app/overlay", `
@@ -201,15 +200,50 @@ metadata:
spec: spec:
replicas: 1000 replicas: 1000
`) `)
_, err := th.makeKustTarget().MakeCustomizedResMap() m, err := th.makeKustTarget().MakeCustomizedResMap()
if err == nil { if err != nil {
t.Fatalf("expected error") t.Fatalf("Err: %v", err)
}
if !strings.Contains(
err.Error(),
"security; file '../shared/deployment-patch.yaml' is not in or below '/app/overlay'") {
t.Fatalf("unexpected error: %s", err)
} }
th.assertActualEqualsExpected(m, `
apiVersion: v1
kind: Service
metadata:
labels:
app: myApp
env: prod
name: a-myService
spec:
ports:
- port: 7002
selector:
app: myApp
backend: bungie
env: prod
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: myApp
env: prod
name: a-myDeployment
spec:
replicas: 1000
selector:
matchLabels:
app: myApp
env: prod
template:
metadata:
labels:
app: myApp
backend: awesome
env: prod
spec:
containers:
- image: whatever
name: whatever
`)
} }
func TestSmallOverlayJSONPatch(t *testing.T) { func TestSmallOverlayJSONPatch(t *testing.T) {