@@ -26,7 +26,9 @@ package secureloader
2626
2727import (
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.
5254type 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.
7476func (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