Skip to content

Commit 711780c

Browse files
authored
Merge pull request #663 from fluxcd/helm-safe-dir-loader
2 parents 8593d58 + 9a17fd5 commit 711780c

32 files changed

+1727
-62
lines changed

controllers/helmchart_controller.go

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828
"strings"
2929
"time"
3030

31-
securejoin "github.com/cyphar/filepath-securejoin"
3231
helmgetter "helm.sh/helm/v3/pkg/getter"
3332
helmrepo "helm.sh/helm/v3/pkg/repo"
3433
corev1 "k8s.io/api/core/v1"
@@ -609,18 +608,6 @@ func (r *HelmChartReconciler) buildFromTarballArtifact(ctx context.Context, obj
609608
}
610609
}
611610

612-
// Calculate (secure) absolute chart path
613-
chartPath, err := securejoin.SecureJoin(sourceDir, obj.Spec.Chart)
614-
if err != nil {
615-
e := &serror.Stalling{
616-
Err: fmt.Errorf("path calculation for chart '%s' failed: %w", obj.Spec.Chart, err),
617-
Reason: "IllegalPath",
618-
}
619-
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
620-
// We are unable to recover from this change without a change in generation
621-
return sreconcile.ResultEmpty, e
622-
}
623-
624611
// Setup dependency manager
625612
dm := chart.NewDependencyManager(
626613
chart.WithRepositoryCallback(r.namespacedChartRepositoryCallback(ctx, obj.GetNamespace())),
@@ -673,7 +660,7 @@ func (r *HelmChartReconciler) buildFromTarballArtifact(ctx context.Context, obj
673660
cb := chart.NewLocalBuilder(dm)
674661
build, err := cb.Build(ctx, chart.LocalReference{
675662
WorkDir: sourceDir,
676-
Path: chartPath,
663+
Path: obj.Spec.Chart,
677664
}, util.TempPathForObj("", ".tgz", obj), opts)
678665
if err != nil {
679666
return sreconcile.ResultEmpty, err

internal/helm/chart/builder.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,25 @@ type LocalReference struct {
4343
// WorkDir used as chroot during build operations.
4444
// File references are not allowed to traverse outside it.
4545
WorkDir string
46-
// Path of the chart on the local filesystem.
46+
// Path of the chart on the local filesystem relative to WorkDir.
4747
Path string
4848
}
4949

5050
// Validate returns an error if the LocalReference does not have
5151
// a Path set.
5252
func (r LocalReference) Validate() error {
53+
if r.WorkDir == "" {
54+
return fmt.Errorf("no work dir set for local chart reference")
55+
}
5356
if r.Path == "" {
5457
return fmt.Errorf("no path set for local chart reference")
5558
}
59+
if !filepath.IsAbs(r.WorkDir) {
60+
return fmt.Errorf("local chart reference work dir is expected to be absolute")
61+
}
62+
if filepath.IsAbs(r.Path) {
63+
return fmt.Errorf("local chart reference path is expected to be relative")
64+
}
5665
return nil
5766
}
5867

internal/helm/chart/builder_local.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ import (
2424

2525
"github.com/Masterminds/semver/v3"
2626
securejoin "github.com/cyphar/filepath-securejoin"
27-
"helm.sh/helm/v3/pkg/chart/loader"
2827
"sigs.k8s.io/yaml"
2928

3029
"github.com/fluxcd/pkg/runtime/transform"
30+
31+
"github.com/fluxcd/source-controller/internal/helm/chart/secureloader"
3132
)
3233

3334
type localChartBuilder struct {
@@ -75,7 +76,11 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
7576

7677
// Load the chart metadata from the LocalReference to ensure it points
7778
// to a chart
78-
curMeta, err := LoadChartMetadata(localRef.Path)
79+
securePath, err := securejoin.SecureJoin(localRef.WorkDir, localRef.Path)
80+
if err != nil {
81+
return nil, &BuildError{Reason: ErrChartReference, Err: err}
82+
}
83+
curMeta, err := LoadChartMetadata(securePath)
7984
if err != nil {
8085
return nil, &BuildError{Reason: ErrChartReference, Err: err}
8186
}
@@ -101,7 +106,7 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
101106
result.Version = ver.String()
102107
}
103108

104-
isChartDir := pathIsDir(localRef.Path)
109+
isChartDir := pathIsDir(securePath)
105110
requiresPackaging := isChartDir || opts.VersionMetadata != "" || len(opts.GetValuesFiles()) != 0
106111

107112
// If all the following is true, we do not need to package the chart:
@@ -127,7 +132,7 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
127132
// If the chart at the path is already packaged and no custom values files
128133
// options are set, we can copy the chart without making modifications
129134
if !requiresPackaging {
130-
if err = copyFileToPath(localRef.Path, p); err != nil {
135+
if err = copyFileToPath(securePath, p); err != nil {
131136
return result, &BuildError{Reason: ErrChartPull, Err: err}
132137
}
133138
result.Path = p
@@ -145,15 +150,16 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
145150
// At this point we are certain we need to load the chart;
146151
// either to package it because it originates from a directory,
147152
// or because we have merged values and need to repackage
148-
chart, err := loader.Load(localRef.Path)
153+
loadedChart, err := secureloader.Load(localRef.WorkDir, localRef.Path)
149154
if err != nil {
150155
return result, &BuildError{Reason: ErrChartPackage, Err: err}
151156
}
157+
152158
// Set earlier resolved version (with metadata)
153-
chart.Metadata.Version = result.Version
159+
loadedChart.Metadata.Version = result.Version
154160

155161
// Overwrite default values with merged values, if any
156-
if ok, err = OverwriteChartDefaultValues(chart, mergedValues); ok || err != nil {
162+
if ok, err = OverwriteChartDefaultValues(loadedChart, mergedValues); ok || err != nil {
157163
if err != nil {
158164
return result, &BuildError{Reason: ErrValuesFilesMerge, Err: err}
159165
}
@@ -166,13 +172,13 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
166172
err = fmt.Errorf("local chart builder requires dependency manager for unpackaged charts")
167173
return result, &BuildError{Reason: ErrDependencyBuild, Err: err}
168174
}
169-
if result.ResolvedDependencies, err = b.dm.Build(ctx, ref, chart); err != nil {
175+
if result.ResolvedDependencies, err = b.dm.Build(ctx, ref, loadedChart); err != nil {
170176
return result, &BuildError{Reason: ErrDependencyBuild, Err: err}
171177
}
172178
}
173179

174180
// Package the chart
175-
if err = packageToPath(chart, p); err != nil {
181+
if err = packageToPath(loadedChart, p); err != nil {
176182
return result, &BuildError{Reason: ErrChartPackage, Err: err}
177183
}
178184
result.Path = p

internal/helm/chart/builder_local_test.go

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ import (
2626
. "github.com/onsi/gomega"
2727
"github.com/otiai10/copy"
2828
helmchart "helm.sh/helm/v3/pkg/chart"
29-
"helm.sh/helm/v3/pkg/chart/loader"
3029
"helm.sh/helm/v3/pkg/chartutil"
3130
"helm.sh/helm/v3/pkg/repo"
3231

32+
"github.com/fluxcd/source-controller/internal/helm/chart/secureloader"
3333
"github.com/fluxcd/source-controller/internal/helm/repository"
3434
)
3535

@@ -86,31 +86,31 @@ func TestLocalBuilder_Build(t *testing.T) {
8686
},
8787
{
8888
name: "invalid local reference - no file",
89-
reference: LocalReference{Path: "/tmp/non-existent-path.xyz"},
89+
reference: LocalReference{WorkDir: "/tmp", Path: "non-existent-path.xyz"},
9090
wantErr: "no such file or directory",
9191
},
9292
{
9393
name: "invalid version metadata",
94-
reference: LocalReference{Path: "./../testdata/charts/helmchart"},
94+
reference: LocalReference{Path: "../testdata/charts/helmchart"},
9595
buildOpts: BuildOptions{VersionMetadata: "^"},
9696
wantErr: "Invalid Metadata string",
9797
},
9898
{
9999
name: "with version metadata",
100-
reference: LocalReference{Path: "./../testdata/charts/helmchart"},
100+
reference: LocalReference{Path: "../testdata/charts/helmchart"},
101101
buildOpts: BuildOptions{VersionMetadata: "foo"},
102102
wantVersion: "0.1.0+foo",
103103
wantPackaged: true,
104104
},
105105
{
106106
name: "already packaged chart",
107-
reference: LocalReference{Path: "./../testdata/charts/helmchart-0.1.0.tgz"},
107+
reference: LocalReference{Path: "../testdata/charts/helmchart-0.1.0.tgz"},
108108
wantVersion: "0.1.0",
109109
wantPackaged: false,
110110
},
111111
{
112112
name: "default values",
113-
reference: LocalReference{Path: "./../testdata/charts/helmchart"},
113+
reference: LocalReference{Path: "../testdata/charts/helmchart"},
114114
wantValues: chartutil.Values{
115115
"replicaCount": float64(1),
116116
},
@@ -119,7 +119,7 @@ func TestLocalBuilder_Build(t *testing.T) {
119119
},
120120
{
121121
name: "with values files",
122-
reference: LocalReference{Path: "./../testdata/charts/helmchart"},
122+
reference: LocalReference{Path: "../testdata/charts/helmchart"},
123123
buildOpts: BuildOptions{
124124
ValuesFiles: []string{"custom-values1.yaml", "custom-values2.yaml"},
125125
},
@@ -145,7 +145,7 @@ fullnameOverride: "full-foo-name-override"`),
145145
},
146146
{
147147
name: "chart with dependencies",
148-
reference: LocalReference{Path: "./../testdata/charts/helmchartwithdeps"},
148+
reference: LocalReference{Path: "../testdata/charts/helmchartwithdeps"},
149149
repositories: map[string]*repository.ChartRepository{
150150
"https://grafana.github.io/helm-charts/": mockRepo(),
151151
},
@@ -164,11 +164,11 @@ fullnameOverride: "full-foo-name-override"`),
164164
},
165165
{
166166
name: "v1 chart with dependencies",
167-
reference: LocalReference{Path: "./../testdata/charts/helmchartwithdeps-v1"},
167+
reference: LocalReference{Path: "../testdata/charts/helmchartwithdeps-v1"},
168168
repositories: map[string]*repository.ChartRepository{
169169
"https://grafana.github.io/helm-charts/": mockRepo(),
170170
},
171-
dependentChartPaths: []string{"./../testdata/charts/helmchart-v1"},
171+
dependentChartPaths: []string{"../testdata/charts/helmchart-v1"},
172172
wantVersion: "0.3.0",
173173
wantPackaged: true,
174174
},
@@ -184,13 +184,23 @@ fullnameOverride: "full-foo-name-override"`),
184184
// Only if the reference is a LocalReference, set the WorkDir.
185185
localRef, ok := tt.reference.(LocalReference)
186186
if ok {
187+
// If the source chart path is valid, copy it into the workdir
188+
// and update the localRef.Path with the copied local chart
189+
// path.
190+
if localRef.Path != "" {
191+
_, err := os.Lstat(localRef.Path)
192+
if err == nil {
193+
helmchartDir := filepath.Join(workDir, "testdata", "charts", filepath.Base(localRef.Path))
194+
g.Expect(copy.Copy(localRef.Path, helmchartDir)).ToNot(HaveOccurred())
195+
}
196+
}
187197
localRef.WorkDir = workDir
188198
tt.reference = localRef
189199
}
190200

191201
// Write value file in the base dir.
192202
for _, f := range tt.valuesFiles {
193-
vPath := filepath.Join(workDir, f.Name)
203+
vPath := filepath.Join(localRef.WorkDir, f.Name)
194204
g.Expect(os.WriteFile(vPath, f.Data, 0644)).ToNot(HaveOccurred())
195205
}
196206

@@ -223,7 +233,7 @@ fullnameOverride: "full-foo-name-override"`),
223233
g.Expect(cb.Path).ToNot(BeEmpty(), "empty Build.Path")
224234

225235
// Load the resulting chart and verify the values.
226-
resultChart, err := loader.Load(cb.Path)
236+
resultChart, err := secureloader.LoadFile(cb.Path)
227237
g.Expect(err).ToNot(HaveOccurred())
228238
g.Expect(resultChart.Metadata.Version).To(Equal(tt.wantVersion))
229239

@@ -241,7 +251,7 @@ func TestLocalBuilder_Build_CachedChart(t *testing.T) {
241251
g.Expect(err).ToNot(HaveOccurred())
242252
defer os.RemoveAll(workDir)
243253

244-
reference := LocalReference{Path: "./../testdata/charts/helmchart"}
254+
testChartPath := "./../testdata/charts/helmchart"
245255

246256
dm := NewDependencyManager()
247257
b := NewLocalBuilder(dm)
@@ -250,6 +260,11 @@ func TestLocalBuilder_Build_CachedChart(t *testing.T) {
250260
g.Expect(err).ToNot(HaveOccurred())
251261
defer os.RemoveAll(tmpDir)
252262

263+
// Copy the source chart into the workdir.
264+
g.Expect(copy.Copy(testChartPath, filepath.Join(workDir, "testdata", "charts", filepath.Base("helmchart")))).ToNot(HaveOccurred())
265+
266+
reference := LocalReference{WorkDir: workDir, Path: testChartPath}
267+
253268
// Build first time.
254269
targetPath := filepath.Join(tmpDir, "chart1.tgz")
255270
buildOpts := BuildOptions{}

internal/helm/chart/builder_remote.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ import (
2525

2626
"github.com/Masterminds/semver/v3"
2727
helmchart "helm.sh/helm/v3/pkg/chart"
28-
"helm.sh/helm/v3/pkg/chart/loader"
2928
"helm.sh/helm/v3/pkg/chartutil"
3029
"sigs.k8s.io/yaml"
3130

3231
"github.com/fluxcd/pkg/runtime/transform"
3332

3433
"github.com/fluxcd/source-controller/internal/fs"
34+
"github.com/fluxcd/source-controller/internal/helm/chart/secureloader"
3535
"github.com/fluxcd/source-controller/internal/helm/repository"
3636
)
3737

@@ -145,7 +145,7 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o
145145

146146
// Load the chart and merge chart values
147147
var chart *helmchart.Chart
148-
if chart, err = loader.LoadArchive(res); err != nil {
148+
if chart, err = secureloader.LoadArchive(res); err != nil {
149149
err = fmt.Errorf("failed to load downloaded chart: %w", err)
150150
return result, &BuildError{Reason: ErrChartPackage, Err: err}
151151
}

internal/helm/chart/builder_remote_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ import (
2727

2828
. "github.com/onsi/gomega"
2929
helmchart "helm.sh/helm/v3/pkg/chart"
30-
"helm.sh/helm/v3/pkg/chart/loader"
3130
"helm.sh/helm/v3/pkg/chartutil"
3231
helmgetter "helm.sh/helm/v3/pkg/getter"
3332

33+
"github.com/fluxcd/source-controller/internal/helm/chart/secureloader"
3434
"github.com/fluxcd/source-controller/internal/helm/repository"
3535
)
3636

@@ -186,7 +186,7 @@ entries:
186186
g.Expect(cb.Path).ToNot(BeEmpty(), "empty Build.Path")
187187

188188
// Load the resulting chart and verify the values.
189-
resultChart, err := loader.Load(cb.Path)
189+
resultChart, err := secureloader.LoadFile(cb.Path)
190190
g.Expect(err).ToNot(HaveOccurred())
191191
g.Expect(resultChart.Metadata.Version).To(Equal(tt.wantVersion))
192192

internal/helm/chart/builder_test.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ import (
2424
"testing"
2525

2626
. "github.com/onsi/gomega"
27-
"helm.sh/helm/v3/pkg/chart/loader"
2827
"helm.sh/helm/v3/pkg/chartutil"
28+
29+
"github.com/fluxcd/source-controller/internal/helm/chart/secureloader"
2930
)
3031

3132
func TestLocalReference_Validate(t *testing.T) {
@@ -35,18 +36,29 @@ func TestLocalReference_Validate(t *testing.T) {
3536
wantErr string
3637
}{
3738
{
38-
name: "ref with path",
39-
ref: LocalReference{Path: "/a/path"},
39+
name: "ref with path and work dir",
40+
ref: LocalReference{WorkDir: "/workdir/", Path: "./a/path"},
4041
},
4142
{
42-
name: "ref with path and work dir",
43-
ref: LocalReference{Path: "/a/path", WorkDir: "/with/a/workdir"},
43+
name: "ref without work dir",
44+
ref: LocalReference{Path: "/a/path"},
45+
wantErr: "no work dir set for local chart reference",
46+
},
47+
{
48+
name: "ref with relative work dir",
49+
ref: LocalReference{WorkDir: "../a/path", Path: "foo"},
50+
wantErr: "local chart reference work dir is expected to be absolute",
4451
},
4552
{
4653
name: "ref without path",
4754
ref: LocalReference{WorkDir: "/just/a/workdir"},
4855
wantErr: "no path set for local chart reference",
4956
},
57+
{
58+
name: "ref with an absolute path",
59+
ref: LocalReference{WorkDir: "/a/path", Path: "/foo"},
60+
wantErr: "local chart reference path is expected to be relative",
61+
},
5062
}
5163
for _, tt := range tests {
5264
t.Run(tt.name, func(t *testing.T) {
@@ -210,7 +222,7 @@ func TestChartBuildResult_String(t *testing.T) {
210222
func Test_packageToPath(t *testing.T) {
211223
g := NewWithT(t)
212224

213-
chart, err := loader.Load("../testdata/charts/helmchart-0.1.0.tgz")
225+
chart, err := secureloader.LoadFile("../testdata/charts/helmchart-0.1.0.tgz")
214226
g.Expect(err).ToNot(HaveOccurred())
215227
g.Expect(chart).ToNot(BeNil())
216228

@@ -219,7 +231,7 @@ func Test_packageToPath(t *testing.T) {
219231
err = packageToPath(chart, out)
220232
g.Expect(err).ToNot(HaveOccurred())
221233
g.Expect(out).To(BeARegularFile())
222-
_, err = loader.Load(out)
234+
_, err = secureloader.LoadFile(out)
223235
g.Expect(err).ToNot(HaveOccurred())
224236
}
225237

0 commit comments

Comments
 (0)