Skip to content

Commit 28edefe

Browse files
committed
helm: add more test coverage for secureloader
Signed-off-by: Hidde Beydals <[email protected]>
1 parent 168a6a3 commit 28edefe

File tree

4 files changed

+535
-107
lines changed

4 files changed

+535
-107
lines changed

internal/helm/chart/secureloader/directory.go

Lines changed: 132 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ package secureloader
2626

2727
import (
2828
"bytes"
29+
"errors"
2930
"fmt"
31+
"io/fs"
3032
"os"
3133
"path/filepath"
3234
"strings"
@@ -51,158 +53,204 @@ var (
5153
// symlinks without including files outside root.
5254
type SecureDirLoader struct {
5355
root string
54-
dir string
56+
path string
5557
maxSize int
5658
}
5759

5860
// NewSecureDirLoader returns a new SecureDirLoader, configured to the scope of the
5961
// root and provided dir. Max size configures the maximum size a file must not
60-
// exceed to be loaded. If 0 it defaults to defaultMaxFileSize, it can be
62+
// exceed to be loaded. If 0 it defaults to DefaultMaxFileSize, it can be
6163
// disabled using a negative integer.
62-
func NewSecureDirLoader(root string, dir string, maxSize int) SecureDirLoader {
64+
func NewSecureDirLoader(root string, path string, maxSize int) SecureDirLoader {
6365
if maxSize == 0 {
6466
maxSize = DefaultMaxFileSize
6567
}
6668
return SecureDirLoader{
6769
root: root,
68-
dir: dir,
70+
path: path,
6971
maxSize: maxSize,
7072
}
7173
}
7274

7375
// Load loads and returns the chart.Chart, or an error.
7476
func (l SecureDirLoader) Load() (*chart.Chart, error) {
75-
return SecureLoadDir(l.root, l.dir, l.maxSize)
77+
return SecureLoadDir(l.root, l.path, l.maxSize)
7678
}
7779

78-
// SecureLoadDir securely loads from a directory, without going outside root.
79-
func SecureLoadDir(root, dir string, maxSize int) (*chart.Chart, error) {
80+
// SecureLoadDir securely loads a chart from the path relative to root, without
81+
// traversing outside root. When maxSize >= 0, files are not allowed to exceed
82+
// this size, or an error is returned.
83+
func SecureLoadDir(root, path string, maxSize int) (*chart.Chart, error) {
8084
root, err := filepath.Abs(root)
8185
if err != nil {
8286
return nil, err
8387
}
8488

85-
topDir, err := filepath.Abs(dir)
89+
// Ensure path is relative
90+
if filepath.IsAbs(path) {
91+
relChartPath, err := filepath.Rel(root, path)
92+
if err != nil {
93+
return nil, err
94+
}
95+
path = relChartPath
96+
}
97+
98+
// Resolve secure absolute path
99+
absChartName, err := securejoin.SecureJoin(root, path)
86100
if err != nil {
87101
return nil, err
88102
}
89103

90-
// Confirm topDir is actually relative to root
91-
if _, err = isSecureSymlinkPath(root, topDir); err != nil {
92-
return nil, fmt.Errorf("cannot load chart from dir: %w", err)
104+
// Load ignore rules
105+
rules, err := secureLoadIgnoreRules(root, path)
106+
if err != nil {
107+
return nil, fmt.Errorf("cannot load ignore rules for chart: %w", err)
93108
}
94109

95-
// Just used for errors
96-
c := &chart.Chart{}
110+
// Lets go for a walk...
111+
fileWalker := newSecureFileWalker(root, absChartName, maxSize, rules)
112+
if err = sympath.Walk(fileWalker.absChartPath, fileWalker.walk); err != nil {
113+
return nil, fmt.Errorf("failed to load files from %s: %w", strings.TrimPrefix(fileWalker.absChartPath, fileWalker.root), err)
114+
}
97115

98-
// Get the absolute location of the .helmignore file
99-
relDirPath, err := filepath.Rel(root, topDir)
116+
loaded, err := loader.LoadFiles(fileWalker.files)
100117
if err != nil {
101-
// We are not expected to be returning this error, as the above call to
102-
// isSecureSymlinkPath already does the same. However, especially
103-
// because we are dealing with security aspects here, we check it
104-
// anyway in case this assumption changes.
105-
return nil, err
118+
return nil, fmt.Errorf("failed to load chart from %s: %w", strings.TrimPrefix(fileWalker.absChartPath, fileWalker.root), err)
106119
}
107-
iFile, err := securejoin.SecureJoin(root, filepath.Join(relDirPath, ignore.HelmIgnore))
120+
return loaded, nil
121+
}
108122

109-
// Load the .helmignore rules
123+
// secureLoadIgnoreRules attempts to load the ignore.HelmIgnore file from the
124+
// chart path relative to root. If the file is a symbolic link, it is evaluated
125+
// with the given root treated as root of the filesystem.
126+
// If the ignore file does not exist, or points to a location outside of root,
127+
// default ignore.Rules are returned. Any error other than fs.ErrNotExist is
128+
// returned.
129+
func secureLoadIgnoreRules(root, chartPath string) (*ignore.Rules, error) {
110130
rules := ignore.Empty()
111-
if _, err = os.Stat(iFile); err == nil {
112-
r, err := ignore.ParseFile(iFile)
113-
if err != nil {
114-
return c, err
131+
132+
iFile, err := securejoin.SecureJoin(root, filepath.Join(chartPath, ignore.HelmIgnore))
133+
if err != nil {
134+
return nil, err
135+
}
136+
_, err = os.Stat(iFile)
137+
if err != nil && !errors.Is(err, fs.ErrNotExist) {
138+
return nil, err
139+
}
140+
if err == nil {
141+
if rules, err = ignore.ParseFile(iFile); err != nil {
142+
return nil, err
115143
}
116-
rules = r
117144
}
145+
118146
rules.AddDefaults()
147+
return rules, nil
148+
}
119149

120-
var files []*loader.BufferedFile
121-
topDir += string(filepath.Separator)
150+
// secureFileWalker does the actual walking over the directory, any file loaded
151+
// by walk is appended to files.
152+
type secureFileWalker struct {
153+
root string
154+
absChartPath string
155+
maxSize int
156+
rules *ignore.Rules
157+
files []*loader.BufferedFile
158+
}
122159

123-
walk := func(name, absoluteName string, fi os.FileInfo, err error) error {
124-
n := strings.TrimPrefix(name, topDir)
125-
if n == "" {
126-
// No need to process top level. Avoid bug with helmignore .* matching
127-
// empty names. See issue 1779.
128-
return nil
129-
}
160+
func newSecureFileWalker(root, absChartPath string, maxSize int, rules *ignore.Rules) *secureFileWalker {
161+
absChartPath = filepath.Clean(absChartPath) + string(filepath.Separator)
162+
return &secureFileWalker{
163+
root: root,
164+
absChartPath: absChartPath,
165+
maxSize: maxSize,
166+
rules: rules,
167+
files: make([]*loader.BufferedFile, 0),
168+
}
169+
}
130170

131-
// Normalize to / since it will also work on Windows
132-
n = filepath.ToSlash(n)
171+
func (w *secureFileWalker) walk(name, absName string, fi os.FileInfo, err error) error {
172+
n := strings.TrimPrefix(name, w.absChartPath)
173+
if n == "" {
174+
// No need to process top level. Avoid bug with helmignore .* matching
175+
// empty names. See issue 1779.
176+
return nil
177+
}
133178

134-
if err != nil {
135-
return err
136-
}
137-
if fi.IsDir() {
138-
// Directory-based ignore rules should involve skipping the entire
139-
// contents of that directory.
140-
if rules.Ignore(n, fi) {
141-
return filepath.SkipDir
142-
}
143-
// Check after excluding ignores to provide the user with an option
144-
// to opt-out from including certain paths.
145-
if _, err := isSecureSymlinkPath(root, absoluteName); err != nil {
146-
return fmt.Errorf("cannot load '%s' directory: %w", n, err)
147-
}
148-
return nil
149-
}
179+
if err != nil {
180+
return err
181+
}
150182

151-
// If a .helmignore file matches, skip this file.
152-
if rules.Ignore(n, fi) {
153-
return nil
154-
}
183+
// Normalize to / since it will also work on Windows
184+
n = filepath.ToSlash(n)
155185

186+
if fi.IsDir() {
187+
// Directory-based ignore rules should involve skipping the entire
188+
// contents of that directory.
189+
if w.rules.Ignore(n, fi) {
190+
return filepath.SkipDir
191+
}
156192
// Check after excluding ignores to provide the user with an option
157193
// to opt-out from including certain paths.
158-
if _, err := isSecureSymlinkPath(root, absoluteName); err != nil {
159-
return fmt.Errorf("cannot load '%s' file: %w", n, err)
194+
if _, err := isSecureAbsolutePath(w.root, absName); err != nil {
195+
return fmt.Errorf("cannot load '%s' directory: %w", n, err)
160196
}
197+
return nil
198+
}
161199

162-
// Irregular files include devices, sockets, and other uses of files that
163-
// are not regular files. In Go they have a file mode type bit set.
164-
// See https://golang.org/pkg/os/#FileMode for examples.
165-
if !fi.Mode().IsRegular() {
166-
return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", n)
167-
}
200+
// If a .helmignore file matches, skip this file.
201+
if w.rules.Ignore(n, fi) {
202+
return nil
203+
}
168204

169-
if fileSize := fi.Size(); maxSize > 0 && fileSize > int64(maxSize) {
170-
return fmt.Errorf("cannot load file %s as file size (%d) exceeds limit (%d)", n, fileSize, maxSize)
171-
}
205+
// Check after excluding ignores to provide the user with an option
206+
// to opt-out from including certain paths.
207+
if _, err := isSecureAbsolutePath(w.root, absName); err != nil {
208+
return fmt.Errorf("cannot load '%s' file: %w", n, err)
209+
}
172210

173-
data, err := os.ReadFile(name)
174-
if err != nil {
175-
return fmt.Errorf("error reading %s: %w", n, err)
176-
}
177-
data = bytes.TrimPrefix(data, utf8bom)
211+
// Irregular files include devices, sockets, and other uses of files that
212+
// are not regular files. In Go they have a file mode type bit set.
213+
// See https://golang.org/pkg/os/#FileMode for examples.
214+
if !fi.Mode().IsRegular() {
215+
return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", n)
216+
}
178217

179-
files = append(files, &loader.BufferedFile{Name: n, Data: data})
180-
return nil
218+
// Confirm size it not outside boundaries
219+
if fileSize := fi.Size(); w.maxSize > 0 && fileSize > int64(w.maxSize) {
220+
return fmt.Errorf("cannot load file %s as file size (%d) exceeds limit (%d)", n, fileSize, w.maxSize)
181221
}
182-
if err = sympath.Walk(topDir, walk); err != nil {
183-
return c, err
222+
223+
data, err := os.ReadFile(absName)
224+
if err != nil {
225+
if pathErr := new(fs.PathError); errors.As(err, &pathErr) {
226+
err = &fs.PathError{Op: pathErr.Op, Path: strings.TrimPrefix(absName, w.root), Err: pathErr.Err}
227+
}
228+
return fmt.Errorf("error reading %s: %w", n, err)
184229
}
185-
return loader.LoadFiles(files)
230+
data = bytes.TrimPrefix(data, utf8bom)
231+
232+
w.files = append(w.files, &loader.BufferedFile{Name: n, Data: data})
233+
return nil
186234
}
187235

188-
// isSecureSymlinkPath attempts to make the given absolute path relative to
236+
// isSecureAbsolutePath attempts to make the given absolute path relative to
189237
// root and securely joins this with root. If the result equals absolute path,
190238
// it is safe to use.
191-
func isSecureSymlinkPath(root, absPath string) (bool, error) {
239+
func isSecureAbsolutePath(root, absPath string) (bool, error) {
192240
root, absPath = filepath.Clean(root), filepath.Clean(absPath)
193241
if root == "/" {
194242
return true, nil
195243
}
196244
unsafePath, err := filepath.Rel(root, absPath)
197245
if err != nil {
198-
return false, fmt.Errorf("cannot calculate path relative to root for resolved symlink")
246+
return false, fmt.Errorf("cannot calculate path relative to root for absolute path")
199247
}
200248
safePath, err := securejoin.SecureJoin(root, unsafePath)
201249
if err != nil {
202-
return false, fmt.Errorf("cannot securely join root with resolved relative symlink path")
250+
return false, fmt.Errorf("cannot securely join root with resolved relative path")
203251
}
204252
if safePath != absPath {
205-
return false, fmt.Errorf("symlink traverses outside root boundary: relative path to root %s", unsafePath)
253+
return false, fmt.Errorf("absolute path traverses outside root boundary: relative path to root %s", unsafePath)
206254
}
207255
return true, nil
208256
}

0 commit comments

Comments
 (0)