420 lines
14 KiB
Go
420 lines
14 KiB
Go
/*
|
|
Copyright 2022 The Flux 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 secureloader
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"github.com/fluxcd/source-controller/internal/helm/chart/secureloader/ignore"
|
|
. "github.com/onsi/gomega"
|
|
"helm.sh/helm/v3/pkg/chart"
|
|
"sigs.k8s.io/yaml"
|
|
)
|
|
|
|
func TestSecureDirLoader_Load(t *testing.T) {
|
|
metadata := chart.Metadata{
|
|
Name: "test",
|
|
APIVersion: "v2",
|
|
Version: "1.0",
|
|
Type: "application",
|
|
}
|
|
|
|
t.Run("chart", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
tmpDir := t.TempDir()
|
|
m := metadata
|
|
b, err := yaml.Marshal(&m)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(os.WriteFile(filepath.Join(tmpDir, "Chart.yaml"), b, 0o644)).To(Succeed())
|
|
|
|
got, err := (NewSecureDirLoader(tmpDir, "", DefaultMaxFileSize)).Load()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(got).ToNot(BeNil())
|
|
g.Expect(got.Name()).To(Equal(m.Name))
|
|
})
|
|
|
|
t.Run("chart with absolute path", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
tmpDir := t.TempDir()
|
|
m := metadata
|
|
b, err := yaml.Marshal(&m)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(os.WriteFile(filepath.Join(tmpDir, "Chart.yaml"), b, 0o644)).To(Succeed())
|
|
|
|
got, err := (NewSecureDirLoader(tmpDir, tmpDir, DefaultMaxFileSize)).Load()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(got).ToNot(BeNil())
|
|
g.Expect(got.Name()).To(Equal(m.Name))
|
|
})
|
|
|
|
t.Run("chart with illegal path", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
m := metadata
|
|
b, err := yaml.Marshal(&m)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(os.WriteFile(filepath.Join(tmpDir, "Chart.yaml"), b, 0o644)).To(Succeed())
|
|
|
|
root := filepath.Join(tmpDir, "root")
|
|
g.Expect(os.Mkdir(root, 0o700)).To(Succeed())
|
|
|
|
got, err := (NewSecureDirLoader(root, "../", DefaultMaxFileSize)).Load()
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(err.Error()).To(ContainSubstring("failed to load chart from /: Chart.yaml file is missing"))
|
|
g.Expect(got).To(BeNil())
|
|
|
|
got, err = (NewSecureDirLoader(root, tmpDir, DefaultMaxFileSize)).Load()
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(err.Error()).To(ContainSubstring("failed to load chart from /: Chart.yaml file is missing"))
|
|
g.Expect(got).To(BeNil())
|
|
})
|
|
|
|
t.Run("chart with .helmignore", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
tmpDir := t.TempDir()
|
|
m := metadata
|
|
b, err := yaml.Marshal(&m)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(os.WriteFile(filepath.Join(tmpDir, "Chart.yaml"), b, 0o644)).To(Succeed())
|
|
g.Expect(os.WriteFile(filepath.Join(tmpDir, ignore.HelmIgnore), []byte("file.txt"), 0o644)).To(Succeed())
|
|
g.Expect(os.WriteFile(filepath.Join(tmpDir, "file.txt"), []byte("not included"), 0o644)).To(Succeed())
|
|
|
|
got, err := (NewSecureDirLoader(tmpDir, "", DefaultMaxFileSize)).Load()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(got).ToNot(BeNil())
|
|
g.Expect(got.Name()).To(Equal(m.Name))
|
|
g.Expect(got.Raw).To(HaveLen(2))
|
|
})
|
|
}
|
|
|
|
func Test_secureLoadIgnoreRules(t *testing.T) {
|
|
t.Run("defaults", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
r, err := secureLoadIgnoreRules("/workdir", "")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(r.Ignore("file.txt", nil)).To(BeFalse())
|
|
g.Expect(r.Ignore("templates/.dotfile", nil)).To(BeTrue())
|
|
})
|
|
|
|
t.Run("with "+ignore.HelmIgnore, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
tmpDir := t.TempDir()
|
|
g.Expect(os.WriteFile(filepath.Join(tmpDir, ignore.HelmIgnore), []byte("file.txt"), 0o644)).To(Succeed())
|
|
|
|
r, err := secureLoadIgnoreRules(tmpDir, "")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(r.Ignore("file.txt", nil)).To(BeTrue())
|
|
g.Expect(r.Ignore("templates/.dotfile", nil)).To(BeTrue())
|
|
g.Expect(r.Ignore("other.txt", nil)).To(BeFalse())
|
|
})
|
|
|
|
t.Run("with chart path and "+ignore.HelmIgnore, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
tmpDir := t.TempDir()
|
|
chartPath := "./sub/chart"
|
|
g.Expect(os.MkdirAll(filepath.Join(tmpDir, chartPath), 0o700)).To(Succeed())
|
|
g.Expect(os.WriteFile(filepath.Join(tmpDir, chartPath, ignore.HelmIgnore), []byte("file.txt"), 0o644)).To(Succeed())
|
|
|
|
r, err := secureLoadIgnoreRules(tmpDir, chartPath)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(r.Ignore("file.txt", nil)).To(BeTrue())
|
|
})
|
|
|
|
t.Run("with relative "+ignore.HelmIgnore+" symlink", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
tmpDir := t.TempDir()
|
|
chartPath := "sub/chart"
|
|
g.Expect(os.MkdirAll(filepath.Join(tmpDir, chartPath), 0o700)).To(Succeed())
|
|
g.Expect(os.WriteFile(filepath.Join(tmpDir, "symlink"), []byte("file.txt"), 0o644)).To(Succeed())
|
|
g.Expect(os.Symlink("../../symlink", filepath.Join(tmpDir, chartPath, ignore.HelmIgnore)))
|
|
|
|
r, err := secureLoadIgnoreRules(tmpDir, chartPath)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(r.Ignore("file.txt", nil)).To(BeTrue())
|
|
})
|
|
|
|
t.Run("with illegal "+ignore.HelmIgnore+" symlink", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
tmpDir := t.TempDir()
|
|
chartPath := "/sub/chart"
|
|
g.Expect(os.MkdirAll(filepath.Join(tmpDir, chartPath), 0o700)).To(Succeed())
|
|
g.Expect(os.WriteFile(filepath.Join(tmpDir, "symlink"), []byte("file.txt"), 0o644)).To(Succeed())
|
|
g.Expect(os.Symlink("../../symlink", filepath.Join(tmpDir, chartPath, ignore.HelmIgnore)))
|
|
|
|
r, err := secureLoadIgnoreRules(filepath.Join(tmpDir, chartPath), "")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(r.Ignore("templates/.dotfile", nil)).To(BeTrue())
|
|
g.Expect(r.Ignore("file.txt", nil)).To(BeFalse())
|
|
})
|
|
|
|
t.Run("with "+ignore.HelmIgnore+" parsing error", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
tmpDir := t.TempDir()
|
|
g.Expect(os.WriteFile(filepath.Join(tmpDir, ignore.HelmIgnore), []byte("**"), 0o644)).To(Succeed())
|
|
|
|
_, err := secureLoadIgnoreRules(tmpDir, "")
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(err.Error()).To(ContainSubstring("syntax is not supported"))
|
|
})
|
|
}
|
|
|
|
func Test_secureFileWalker_walk(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
const (
|
|
root = "/fake/root"
|
|
chartPath = "/fake/root/dir"
|
|
)
|
|
|
|
fakeDirName := "fake-dir"
|
|
fakeFileName := "fake-file"
|
|
fakeDeviceFileName := "fake-device"
|
|
fakeFS := fstest.MapFS{
|
|
fakeDirName: &fstest.MapFile{Mode: fs.ModeDir},
|
|
fakeFileName: &fstest.MapFile{Data: []byte("a couple bytes")},
|
|
fakeDeviceFileName: &fstest.MapFile{Mode: fs.ModeDevice},
|
|
}
|
|
|
|
// Safe to further re-use this for other paths
|
|
fakeDirInfo, err := fakeFS.Stat(fakeDirName)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
fakeFileInfo, err := fakeFS.Stat(fakeFileName)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
fakeDeviceInfo, err := fakeFS.Stat(fakeDeviceFileName)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
t.Run("given name equals top dir", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
w := newSecureFileWalker(root, chartPath, DefaultMaxFileSize, ignore.Empty())
|
|
g.Expect(w.walk(chartPath+"/", chartPath, nil, nil)).To(BeNil())
|
|
})
|
|
|
|
t.Run("given error is returned", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
err := errors.New("error argument")
|
|
got := (&secureFileWalker{}).walk("name", "/name", nil, err)
|
|
g.Expect(got).To(HaveOccurred())
|
|
g.Expect(got).To(Equal(err))
|
|
})
|
|
|
|
t.Run("ignore rule matches dir", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
rules, err := ignore.Parse(strings.NewReader(fakeDirName + "/"))
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
w := newSecureFileWalker(root, chartPath, DefaultMaxFileSize, rules)
|
|
g.Expect(w.walk(filepath.Join(w.absChartPath, fakeDirName), filepath.Join(w.absChartPath, fakeDirName), fakeDirInfo, nil)).To(Equal(fs.SkipDir))
|
|
})
|
|
|
|
t.Run("absolute path match ignored", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
rules, err := ignore.Parse(strings.NewReader(fakeDirName + "/"))
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
w := newSecureFileWalker(root, chartPath, DefaultMaxFileSize, rules)
|
|
g.Expect(w.walk(filepath.Join(w.absChartPath, "symlink"), filepath.Join(w.absChartPath, fakeDirName), fakeDirInfo, nil)).To(BeNil())
|
|
})
|
|
|
|
t.Run("ignore rule not applicable to dir", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
w := newSecureFileWalker(root, chartPath, DefaultMaxFileSize, ignore.Empty())
|
|
g.Expect(w.walk(filepath.Join(w.absChartPath, fakeDirName), filepath.Join(w.absChartPath, fakeDirName), fakeDirInfo, nil)).To(BeNil())
|
|
})
|
|
|
|
t.Run("absolute path outside root", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
w := newSecureFileWalker(root, chartPath, DefaultMaxFileSize, ignore.Empty())
|
|
err := w.walk(filepath.Join(w.absChartPath, fakeDirName), filepath.Join("/fake/another/root/", fakeDirName), fakeDirInfo, nil)
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(err.Error()).To(ContainSubstring("cannot load 'fake-dir' directory: absolute path traverses outside root boundary"))
|
|
})
|
|
|
|
t.Run("dir ignore rules before secure path check", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
rules, err := ignore.Parse(strings.NewReader(fakeDirName + "/"))
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
w := newSecureFileWalker(root, chartPath, DefaultMaxFileSize, rules)
|
|
g.Expect(w.walk(filepath.Join(w.absChartPath, fakeDirName), filepath.Join("/fake/another/root/", fakeDirName), fakeDirInfo, nil)).To(Equal(fs.SkipDir))
|
|
})
|
|
|
|
t.Run("ignore rule matches file", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
rules, err := ignore.Parse(strings.NewReader(fakeFileName))
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
w := newSecureFileWalker(root, chartPath, DefaultMaxFileSize, rules)
|
|
g.Expect(w.walk(filepath.Join(w.absChartPath, fakeFileName), filepath.Join(w.absChartPath, fakeFileName), fakeFileInfo, nil)).To(BeNil())
|
|
})
|
|
|
|
t.Run("file path outside root", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
w := newSecureFileWalker(root, chartPath, DefaultMaxFileSize, ignore.Empty())
|
|
err := w.walk(filepath.Join(w.absChartPath, fakeFileName), filepath.Join("/fake/another/root/", fakeFileName), fakeFileInfo, nil)
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(err.Error()).To(ContainSubstring("cannot load 'fake-file' file: absolute path traverses outside root boundary"))
|
|
})
|
|
|
|
t.Run("irregular file", func(t *testing.T) {
|
|
w := newSecureFileWalker(root, chartPath, DefaultMaxFileSize, ignore.Empty())
|
|
err := w.walk(fakeDeviceFileName, filepath.Join(w.absChartPath), fakeDeviceInfo, nil)
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(err.Error()).To(ContainSubstring("cannot load irregular file fake-device as it has file mode type bits set"))
|
|
})
|
|
|
|
t.Run("file exceeds max size", func(t *testing.T) {
|
|
w := newSecureFileWalker(root, chartPath, 5, ignore.Empty())
|
|
err := w.walk(fakeFileName, filepath.Join(w.absChartPath), fakeFileInfo, nil)
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(err.Error()).To(Equal(fmt.Sprintf("cannot load file fake-file as file size (%d) exceeds limit (%d)", fakeFileInfo.Size(), w.maxSize)))
|
|
})
|
|
|
|
t.Run("file is appended", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
tmpDir := t.TempDir()
|
|
|
|
fileName := "append-file"
|
|
fileData := []byte("append-file-data")
|
|
absFilePath := filepath.Join(tmpDir, fileName)
|
|
g.Expect(os.WriteFile(absFilePath, fileData, 0o644)).To(Succeed())
|
|
fileInfo, err := os.Lstat(absFilePath)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
w := newSecureFileWalker(tmpDir, tmpDir, DefaultMaxFileSize, ignore.Empty())
|
|
g.Expect(w.walk(fileName, absFilePath, fileInfo, nil)).To(Succeed())
|
|
g.Expect(w.files).To(HaveLen(1))
|
|
g.Expect(w.files[0].Name).To(Equal(fileName))
|
|
g.Expect(w.files[0].Data).To(Equal(fileData))
|
|
})
|
|
|
|
t.Run("utf8bom is removed from file data", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
tmpDir := t.TempDir()
|
|
|
|
fileName := "append-file"
|
|
fileData := []byte("append-file-data")
|
|
fileDataWithBom := append(utf8bom, fileData...)
|
|
absFilePath := filepath.Join(tmpDir, fileName)
|
|
g.Expect(os.WriteFile(absFilePath, fileDataWithBom, 0o644)).To(Succeed())
|
|
fileInfo, err := os.Lstat(absFilePath)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
w := newSecureFileWalker(tmpDir, tmpDir, DefaultMaxFileSize, ignore.Empty())
|
|
g.Expect(w.walk(fileName, absFilePath, fileInfo, nil)).To(Succeed())
|
|
g.Expect(w.files).To(HaveLen(1))
|
|
g.Expect(w.files[0].Name).To(Equal(fileName))
|
|
g.Expect(w.files[0].Data).To(Equal(fileData))
|
|
})
|
|
|
|
t.Run("file does not exist", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
tmpDir := t.TempDir()
|
|
|
|
w := newSecureFileWalker(tmpDir, tmpDir, DefaultMaxFileSize, ignore.Empty())
|
|
err := w.walk(filepath.Join(w.absChartPath, "invalid"), filepath.Join(w.absChartPath, "invalid"), fakeFileInfo, nil)
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(errors.Is(err, fs.ErrNotExist)).To(BeTrue())
|
|
g.Expect(err.Error()).To(ContainSubstring("error reading invalid: open /invalid: no such file or directory"))
|
|
})
|
|
}
|
|
|
|
func Test_isSecureAbsolutePath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
root string
|
|
absPath string
|
|
safe bool
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "absolute path in root",
|
|
root: "/",
|
|
absPath: "/bar/",
|
|
safe: true,
|
|
},
|
|
|
|
{
|
|
name: "abs path not relative to root",
|
|
root: "/working/dir",
|
|
absPath: "/working/in/another/dir",
|
|
safe: false,
|
|
wantErr: "absolute path traverses outside root boundary",
|
|
},
|
|
{
|
|
name: "abs path relative to root",
|
|
root: "/working/dir/",
|
|
absPath: "/working/dir/path",
|
|
safe: true,
|
|
},
|
|
{
|
|
name: "illegal abs path",
|
|
root: "/working/dir",
|
|
absPath: "/working/dir/../but/not/really",
|
|
safe: false,
|
|
wantErr: "absolute path traverses outside root boundary",
|
|
},
|
|
{
|
|
name: "illegal root",
|
|
root: "working/dir/",
|
|
absPath: "/working/dir",
|
|
safe: false,
|
|
wantErr: "cannot calculate path relative to root for absolute path",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
got, err := isSecureAbsolutePath(tt.root, tt.absPath)
|
|
g.Expect(got).To(Equal(tt.safe))
|
|
if tt.wantErr != "" {
|
|
g.Expect(err).To(HaveOccurred())
|
|
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
|
return
|
|
}
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
}
|
|
}
|