Skip to content

Commit 4eae37c

Browse files
authored
feat(misconf): support symlinks inside of Helm archives (#6621)
1 parent b7a0a13 commit 4eae37c

File tree

5 files changed

+129
-41
lines changed

5 files changed

+129
-41
lines changed

pkg/iac/scanners/helm/parser/parser.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222
"helm.sh/helm/v3/pkg/releaseutil"
2323

2424
"github.com/aquasecurity/trivy/pkg/iac/debug"
25-
detection2 "github.com/aquasecurity/trivy/pkg/iac/detection"
25+
"github.com/aquasecurity/trivy/pkg/iac/detection"
2626
"github.com/aquasecurity/trivy/pkg/iac/scanners/options"
2727
)
2828

@@ -133,7 +133,7 @@ func (p *Parser) ParseFS(ctx context.Context, target fs.FS, path string) error {
133133
return nil
134134
}
135135

136-
if detection2.IsArchive(path) {
136+
if detection.IsArchive(path) {
137137
tarFS, err := p.addTarToFS(path)
138138
if errors.Is(err, errSkipFS) {
139139
// an unpacked Chart already exists
@@ -320,5 +320,5 @@ func (p *Parser) required(path string, workingFS fs.FS) bool {
320320
return false
321321
}
322322

323-
return detection2.IsType(path, bytes.NewReader(content), detection2.FileTypeHelm)
323+
return detection.IsType(path, bytes.NewReader(content), detection.FileTypeHelm)
324324
}

pkg/iac/scanners/helm/parser/parser_tar.go

+93-29
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ package parser
22

33
import (
44
"archive/tar"
5-
"bytes"
65
"compress/gzip"
76
"errors"
87
"fmt"
98
"io"
109
"io/fs"
1110
"os"
11+
"path"
1212
"path/filepath"
1313

1414
"github.com/liamg/memoryfs"
@@ -18,18 +18,18 @@ import (
1818

1919
var errSkipFS = errors.New("skip parse FS")
2020

21-
func (p *Parser) addTarToFS(path string) (fs.FS, error) {
21+
func (p *Parser) addTarToFS(archivePath string) (fs.FS, error) {
2222
tarFS := memoryfs.CloneFS(p.workingFS)
2323

24-
file, err := tarFS.Open(path)
24+
file, err := tarFS.Open(archivePath)
2525
if err != nil {
2626
return nil, fmt.Errorf("failed to open tar: %w", err)
2727
}
2828
defer file.Close()
2929

3030
var tr *tar.Reader
3131

32-
if detection.IsZip(path) {
32+
if detection.IsZip(archivePath) {
3333
zipped, err := gzip.NewReader(file)
3434
if err != nil {
3535
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
@@ -41,6 +41,7 @@ func (p *Parser) addTarToFS(path string) (fs.FS, error) {
4141
}
4242

4343
checkExistedChart := true
44+
symlinks := make(map[string]string)
4445

4546
for {
4647
header, err := tr.Next()
@@ -51,61 +52,124 @@ func (p *Parser) addTarToFS(path string) (fs.FS, error) {
5152
return nil, fmt.Errorf("failed to get next entry: %w", err)
5253
}
5354

55+
name := filepath.ToSlash(header.Name)
56+
5457
if checkExistedChart {
5558
// Do not add archive files to FS if the chart already exists
5659
// This can happen when the source chart is located next to an archived chart (with the `helm package` command)
5760
// The first level folder in the archive is equal to the Chart name
58-
if _, err := tarFS.Stat(filepath.Dir(path) + "/" + filepath.Dir(header.Name)); err == nil {
61+
if _, err := tarFS.Stat(path.Dir(archivePath) + "/" + path.Dir(name)); err == nil {
5962
return nil, errSkipFS
6063
}
6164
checkExistedChart = false
6265
}
6366

6467
// get the individual path and extract to the current directory
65-
entryPath := header.Name
68+
targetPath := path.Join(path.Dir(archivePath), path.Clean(name))
69+
70+
link := filepath.ToSlash(header.Linkname)
6671

6772
switch header.Typeflag {
6873
case tar.TypeDir:
69-
if err := tarFS.MkdirAll(entryPath, os.FileMode(header.Mode)); err != nil && !errors.Is(err, fs.ErrExist) {
74+
if err := tarFS.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil && !errors.Is(err, fs.ErrExist) {
7075
return nil, err
7176
}
7277
case tar.TypeReg:
73-
writePath := filepath.Dir(path) + "/" + entryPath
74-
p.debug.Log("Unpacking tar entry %s", writePath)
75-
76-
_ = tarFS.MkdirAll(filepath.Dir(writePath), fs.ModePerm)
77-
78-
buf, err := copyChunked(tr, 1024)
79-
if err != nil {
78+
p.debug.Log("Unpacking tar entry %s", targetPath)
79+
if err := copyFile(tarFS, tr, targetPath); err != nil {
8080
return nil, err
8181
}
82-
83-
p.debug.Log("writing file contents to %s", writePath)
84-
if err := tarFS.WriteFile(writePath, buf.Bytes(), fs.ModePerm); err != nil {
85-
return nil, fmt.Errorf("write file error: %w", err)
82+
case tar.TypeSymlink:
83+
if path.IsAbs(link) {
84+
p.debug.Log("Symlink %s is absolute, skipping", link)
85+
continue
8686
}
87+
88+
symlinks[targetPath] = path.Join(path.Dir(targetPath), link) // nolint:gosec // virtual file system is used
8789
default:
8890
return nil, fmt.Errorf("header type %q is not supported", header.Typeflag)
8991
}
9092
}
9193

92-
if err := tarFS.Remove(path); err != nil {
93-
return nil, fmt.Errorf("failed to remove tar from FS: %w", err)
94+
for target, link := range symlinks {
95+
if err := copySymlink(tarFS, link, target); err != nil {
96+
return nil, fmt.Errorf("copy symlink error: %w", err)
97+
}
98+
}
99+
100+
if err := tarFS.Remove(archivePath); err != nil {
101+
return nil, fmt.Errorf("remove tar from FS error: %w", err)
94102
}
95103

96104
return tarFS, nil
97105
}
98106

99-
func copyChunked(src io.Reader, chunkSize int64) (*bytes.Buffer, error) {
100-
buf := new(bytes.Buffer)
101-
for {
102-
if _, err := io.CopyN(buf, src, chunkSize); err != nil {
103-
if errors.Is(err, io.EOF) {
104-
break
105-
}
106-
return nil, fmt.Errorf("failed to copy: %w", err)
107+
func copySymlink(fsys *memoryfs.FS, src, dst string) error {
108+
fi, err := fsys.Stat(src)
109+
if err != nil {
110+
return nil
111+
}
112+
if fi.IsDir() {
113+
if err := copyDir(fsys, src, dst); err != nil {
114+
return fmt.Errorf("copy dir error: %w", err)
115+
}
116+
return nil
117+
}
118+
119+
if err := copyFileLazy(fsys, src, dst); err != nil {
120+
return fmt.Errorf("copy file error: %w", err)
121+
}
122+
123+
return nil
124+
}
125+
126+
func copyFile(fsys *memoryfs.FS, src io.Reader, dst string) error {
127+
if err := fsys.MkdirAll(path.Dir(dst), fs.ModePerm); err != nil && !errors.Is(err, fs.ErrExist) {
128+
return fmt.Errorf("mkdir error: %w", err)
129+
}
130+
131+
b, err := io.ReadAll(src)
132+
if err != nil {
133+
return fmt.Errorf("read error: %w", err)
134+
}
135+
136+
if err := fsys.WriteFile(dst, b, fs.ModePerm); err != nil {
137+
return fmt.Errorf("write file error: %w", err)
138+
}
139+
140+
return nil
141+
}
142+
143+
func copyDir(fsys *memoryfs.FS, src, dst string) error {
144+
walkFn := func(filePath string, entry fs.DirEntry, err error) error {
145+
if err != nil {
146+
return err
107147
}
148+
149+
if entry.IsDir() {
150+
return nil
151+
}
152+
153+
dst := path.Join(dst, filePath[len(src):])
154+
155+
if err := copyFileLazy(fsys, filePath, dst); err != nil {
156+
return fmt.Errorf("copy file error: %w", err)
157+
}
158+
return nil
108159
}
109160

110-
return buf, nil
161+
return fs.WalkDir(fsys, src, walkFn)
162+
}
163+
164+
func copyFileLazy(fsys *memoryfs.FS, src, dst string) error {
165+
if err := fsys.MkdirAll(path.Dir(dst), fs.ModePerm); err != nil && !errors.Is(err, fs.ErrExist) {
166+
return fmt.Errorf("mkdir error: %w", err)
167+
}
168+
return fsys.WriteLazyFile(dst, func() (io.Reader, error) {
169+
f, err := fsys.Open(src)
170+
if err != nil {
171+
return nil, err
172+
}
173+
return f, nil
174+
}, fs.ModePerm)
111175
}

pkg/iac/scanners/helm/parser/parser_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,30 @@ func TestParseFS(t *testing.T) {
2222
}
2323
assert.Equal(t, expectedFiles, p.filepaths)
2424
})
25+
26+
t.Run("archive with symlinks", func(t *testing.T) {
27+
// mkdir -p chart && cd $_
28+
// touch Chart.yaml
29+
// mkdir -p dir && cp -p Chart.yaml dir/Chart.yaml
30+
// mkdir -p sym-to-file && ln -s ../Chart.yaml sym-to-file/Chart.yaml
31+
// ln -s dir sym-to-dir
32+
// mkdir rec-sym && touch rec-sym/Chart.yaml
33+
// ln -s . ./rec-sym/a
34+
// cd .. && tar -czvf chart.tar.gz chart && rm -rf chart
35+
p, err := New(".")
36+
require.NoError(t, err)
37+
38+
fsys := os.DirFS(filepath.Join("testdata", "archive-with-symlinks"))
39+
require.NoError(t, p.ParseFS(context.TODO(), fsys, "chart.tar.gz"))
40+
41+
expectedFiles := []string{
42+
"chart/Chart.yaml",
43+
"chart/dir/Chart.yaml",
44+
"chart/rec-sym/Chart.yaml",
45+
"chart/rec-sym/a/Chart.yaml",
46+
"chart/sym-to-dir/Chart.yaml",
47+
"chart/sym-to-file/Chart.yaml",
48+
}
49+
assert.Equal(t, expectedFiles, p.filepaths)
50+
})
2551
}
Binary file not shown.

pkg/iac/scanners/helm/test/parser_test.go

+7-9
Original file line numberDiff line numberDiff line change
@@ -111,16 +111,14 @@ func Test_tar_is_chart(t *testing.T) {
111111
}
112112

113113
for _, test := range tests {
114+
t.Run(test.testName, func(t *testing.T) {
115+
testPath := filepath.Join("testdata", test.archiveFile)
116+
file, err := os.Open(testPath)
117+
require.NoError(t, err)
118+
defer file.Close()
114119

115-
t.Logf("Running test: %s", test.testName)
116-
testPath := filepath.Join("testdata", test.archiveFile)
117-
file, err := os.Open(testPath)
118-
defer func() { _ = file.Close() }()
119-
require.NoError(t, err)
120-
121-
assert.Equal(t, test.isHelmChart, detection.IsHelmChartArchive(test.archiveFile, file))
122-
123-
_ = file.Close()
120+
assert.Equal(t, test.isHelmChart, detection.IsHelmChartArchive(test.archiveFile, file))
121+
})
124122
}
125123
}
126124

0 commit comments

Comments
 (0)