Skip to content

Commit f1b7026

Browse files
authored
Restrict manifest paths to be in the kustomize directory tree (#29)
* Restrict manifest paths to be in the kustomize directory tree This is an added security measure so that the user cannot specify a manifest path that is not related to their Kustomize configuration. Relates: stolostron/backlog#18236 Signed-off-by: mprahl <[email protected]> * Fix a typo in a test name Signed-off-by: mprahl <[email protected]>
1 parent 478b898 commit f1b7026

File tree

7 files changed

+167
-15
lines changed

7 files changed

+167
-15
lines changed

cmd/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"io/ioutil"
77
"os"
8+
"path"
89

910
"github.com/open-cluster-management/policy-generator-plugin/internal"
1011
"github.com/spf13/pflag"
@@ -50,13 +51,18 @@ func errorAndExit(msg string, formatArgs ...interface{}) {
5051
// It reads the file, processes and validates the contents, uses the contents to
5152
// generate policies, and returns the generated policies as a byte array.
5253
func processGeneratorConfig(filePath string) []byte {
54+
cwd, err := os.Getwd()
55+
if err != nil {
56+
errorAndExit("failed to determine the current directory: %v", err)
57+
}
58+
5359
p := internal.Plugin{}
5460
fileData, err := ioutil.ReadFile(filePath)
5561
if err != nil {
5662
errorAndExit("failed to read file '%s': %s", filePath, err)
5763
}
5864

59-
err = p.Config(fileData)
65+
err = p.Config(fileData, path.Dir(cwd))
6066
if err != nil {
6167
errorAndExit("error processing the PolicyGenerator file '%s': %s", filePath, err)
6268
}

docs/policygenerator-reference.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ policies:
6363
# Required. The list of Kubernetes resource object manifests to include in the policy.
6464
manifests:
6565
# Required. Path to a single file or a flat directory of files relative to the
66-
# kustomization.yaml file.
66+
# kustomization.yaml file. This path cannot be in a directory outside of the directory with
67+
# the kustomization.yaml file. Subdirectories within the directory with kustomization.yaml
68+
# file are allowed.
6769
- path: ""
6870
# Optional. (See policy[0].complianceType for description.)
6971
complianceType: "musthave"

internal/plugin.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ type Plugin struct {
3838
Policies []types.PolicyConfig `json:"policies" yaml:"policies"`
3939
// A set of all placement rule names that have been processed or generated
4040
allPlrs map[string]bool
41+
// The base of the directory tree to restrict all manifest files to be within
42+
baseDirectory string
4143
// This is a mapping of cluster selectors formatted as the return value of getCsKey to placement
4244
// rule names. This is used to find common cluster selectors that can be consolidated to a
4345
// single placement rule.
@@ -58,7 +60,7 @@ var defaults = types.PolicyDefaults{
5860

5961
// Config validates the input PolicyGenerator configuration, applies any missing defaults, and
6062
// configures the Policy object.
61-
func (p *Plugin) Config(config []byte) error {
63+
func (p *Plugin) Config(config []byte, baseDirectory string) error {
6264
err := yaml.Unmarshal(config, p)
6365
const errTemplate = "the PolicyGenerator configuration file is invalid: %w"
6466
if err != nil {
@@ -72,6 +74,8 @@ func (p *Plugin) Config(config []byte) error {
7274
}
7375
p.applyDefaults(unmarshaledConfig)
7476

77+
p.baseDirectory = baseDirectory
78+
7579
return p.assertValidConfig()
7680
}
7781

@@ -353,6 +357,11 @@ func (p *Plugin) assertValidConfig() error {
353357
if err != nil {
354358
return fmt.Errorf("could not read the manifest path %s", manifest.Path)
355359
}
360+
361+
err = verifyManifestPath(p.baseDirectory, manifest.Path)
362+
if err != nil {
363+
return err
364+
}
356365
}
357366

358367
if policy.Name == "" {

internal/plugin_config_test.go

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ policies:
7878
)
7979

8080
p := Plugin{}
81-
err := p.Config([]byte(exampleConfig))
81+
err := p.Config([]byte(exampleConfig), tmpDir)
8282
if err != nil {
8383
t.Fatal(err.Error())
8484
}
@@ -165,7 +165,7 @@ policies:
165165
configMapPath,
166166
)
167167
p := Plugin{}
168-
err := p.Config([]byte(defaultsConfig))
168+
err := p.Config([]byte(defaultsConfig), tmpDir)
169169
if err != nil {
170170
t.Fatal(err.Error())
171171
}
@@ -214,7 +214,7 @@ policies:
214214
- path: input/configmap.yaml
215215
`
216216
p := Plugin{}
217-
err := p.Config([]byte(config))
217+
err := p.Config([]byte(config), "")
218218
if err == nil {
219219
t.Fatal("Expected an error but did not get one")
220220
}
@@ -223,7 +223,7 @@ policies:
223223
assertEqual(t, err.Error(), expected)
224224
}
225225

226-
func TestCreateInvalidPolicyName(t *testing.T) {
226+
func TestConfigInvalidPolicyName(t *testing.T) {
227227
t.Parallel()
228228
tmpDir := t.TempDir()
229229
createConfigMap(t, tmpDir, "configmap.yaml")
@@ -247,7 +247,7 @@ policies:
247247
)
248248

249249
p := Plugin{}
250-
err := p.Config([]byte(defaultsConfig))
250+
err := p.Config([]byte(defaultsConfig), tmpDir)
251251
if err == nil {
252252
t.Fatal("Expected an error but did not get one")
253253
}
@@ -267,7 +267,7 @@ policyDefaults:
267267
namespace: my-policies
268268
`
269269
p := Plugin{}
270-
err := p.Config([]byte(config))
270+
err := p.Config([]byte(config), "")
271271
if err == nil {
272272
t.Fatal("Expected an error but did not get one")
273273
}
@@ -276,6 +276,43 @@ policyDefaults:
276276
assertEqual(t, err.Error(), expected)
277277
}
278278

279+
func TestConfigInvalidPath(t *testing.T) {
280+
t.Parallel()
281+
tmpDir := t.TempDir()
282+
createConfigMap(t, tmpDir, "configmap.yaml")
283+
configMapPath := path.Join(tmpDir, "configmap.yaml")
284+
policyNS := "my-policies"
285+
policyName := "policy-app-config"
286+
defaultsConfig := fmt.Sprintf(
287+
`
288+
apiVersion: policy.open-cluster-management.io/v1
289+
kind: PolicyGenerator
290+
metadata:
291+
name: policy-generator-name
292+
policyDefaults:
293+
namespace: %s
294+
policies:
295+
- name: %s
296+
manifests:
297+
- path: %s
298+
`,
299+
policyNS, policyName, configMapPath,
300+
)
301+
302+
p := Plugin{}
303+
// Provide a base directory that isn't in the same directory tree as tmpDir.
304+
baseDir := t.TempDir()
305+
err := p.Config([]byte(defaultsConfig), baseDir)
306+
if err == nil {
307+
t.Fatal("Expected an error but did not get one")
308+
}
309+
310+
expected := fmt.Sprintf(
311+
"the manifest path %s is not in the same directory tree as the kustomization.yaml file", configMapPath,
312+
)
313+
assertEqual(t, err.Error(), expected)
314+
}
315+
279316
func TestConfigMultiplePlacements(t *testing.T) {
280317
t.Parallel()
281318
const config = `
@@ -295,7 +332,7 @@ policies:
295332
- path: input/configmap.yaml
296333
`
297334
p := Plugin{}
298-
err := p.Config([]byte(config))
335+
err := p.Config([]byte(config), "")
299336
if err == nil {
300337
t.Fatal("Expected an error but did not get one")
301338
}
@@ -331,7 +368,7 @@ policies:
331368
path.Join(tmpDir, "configmap2.yaml"),
332369
)
333370
p := Plugin{}
334-
err := p.Config([]byte(config))
371+
err := p.Config([]byte(config), tmpDir)
335372
if err == nil {
336373
t.Fatal("Expected an error but did not get one")
337374
}
@@ -352,7 +389,7 @@ policies:
352389
- name: policy-app-config
353390
`
354391
p := Plugin{}
355-
err := p.Config([]byte(config))
392+
err := p.Config([]byte(config), "")
356393
if err == nil {
357394
t.Fatal("Expected an error but did not get one")
358395
}
@@ -381,7 +418,7 @@ policies:
381418
manifestPath,
382419
)
383420
p := Plugin{}
384-
err := p.Config([]byte(config))
421+
err := p.Config([]byte(config), tmpDir)
385422
if err == nil {
386423
t.Fatal("Expected an error but did not get one")
387424
}
@@ -409,7 +446,7 @@ policies:
409446
path.Join(tmpDir, "configmap.yaml"),
410447
)
411448
p := Plugin{}
412-
err := p.Config([]byte(config))
449+
err := p.Config([]byte(config), tmpDir)
413450
if err == nil {
414451
t.Fatal("Expected an error but did not get one")
415452
}
@@ -442,7 +479,7 @@ policies:
442479
path.Join(tmpDir, "configmap.yaml"),
443480
)
444481
p := Plugin{}
445-
err := p.Config([]byte(config))
482+
err := p.Config([]byte(config), tmpDir)
446483
if err == nil {
447484
t.Fatal("Expected an error but did not get one")
448485
}

internal/plugin_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func TestGenerate(t *testing.T) {
1616
tmpDir := t.TempDir()
1717
createConfigMap(t, tmpDir, "configmap.yaml")
1818
p := Plugin{}
19+
p.baseDirectory = tmpDir
1920
p.PlacementBindingDefaults.Name = "my-placement-binding"
2021
p.PolicyDefaults.Placement.Name = "my-placement-rule"
2122
p.PolicyDefaults.Namespace = "my-policies"
@@ -158,6 +159,7 @@ func TestGenerateSeparateBindings(t *testing.T) {
158159
tmpDir := t.TempDir()
159160
createConfigMap(t, tmpDir, "configmap.yaml")
160161
p := Plugin{}
162+
p.baseDirectory = tmpDir
161163
p.PolicyDefaults.Namespace = "my-policies"
162164
policyConf := types.PolicyConfig{
163165
Name: "policy-app-config",
@@ -305,6 +307,7 @@ func TestGenerateMissingBindingName(t *testing.T) {
305307
tmpDir := t.TempDir()
306308
createConfigMap(t, tmpDir, "configmap.yaml")
307309
p := Plugin{}
310+
p.baseDirectory = tmpDir
308311
p.PlacementBindingDefaults.Name = ""
309312
p.PolicyDefaults.Placement.Name = "my-placement-rule"
310313
p.PolicyDefaults.Namespace = "my-policies"

internal/utils.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"io/ioutil"
1010
"os"
1111
"path"
12+
"path/filepath"
13+
"strings"
1214

1315
"github.com/open-cluster-management/policy-generator-plugin/internal/expanders"
1416
"github.com/open-cluster-management/policy-generator-plugin/internal/types"
@@ -280,3 +282,30 @@ func unmarshalManifestBytes(manifestBytes []byte) (*[]map[string]interface{}, er
280282

281283
return &yamlDocs, nil
282284
}
285+
286+
// verifyManifestPath verifies that the manifest path is in the directory tree under baseDirectory.
287+
// An error is returned if it is not or the paths couldn't be properly resolved.
288+
func verifyManifestPath(baseDirectory string, manifestPath string) error {
289+
absPath, err := filepath.Abs(manifestPath)
290+
if err != nil {
291+
return fmt.Errorf("could not resolve the manifest path %s to an absolute path", manifestPath)
292+
}
293+
294+
relPath, err := filepath.Rel(baseDirectory, absPath)
295+
if err != nil {
296+
return fmt.Errorf(
297+
"could not resolve the manifest path %s to a relative path from the kustomization.yaml file",
298+
manifestPath,
299+
)
300+
}
301+
302+
parDir := ".." + string(filepath.Separator)
303+
if strings.HasPrefix(relPath, parDir) || relPath == ".." {
304+
return fmt.Errorf(
305+
"the manifest path %s is not in the same directory tree as the kustomization.yaml file",
306+
manifestPath,
307+
)
308+
}
309+
310+
return nil
311+
}

internal/utils_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package internal
44
import (
55
"fmt"
66
"io/ioutil"
7+
"os"
78
"path"
89
"reflect"
910
"testing"
@@ -672,3 +673,68 @@ func TestUnmarshalManifestFileNotObject(t *testing.T) {
672673
)
673674
assertEqual(t, err.Error(), expected)
674675
}
676+
677+
// nolint: paralleltest
678+
func TestVerifyManifestPath(t *testing.T) {
679+
baseDirectory := t.TempDir()
680+
cwd, err := os.Getwd()
681+
if err != nil {
682+
t.Fatalf("Failed to get the current working directory: %v", err)
683+
}
684+
685+
defer func() {
686+
err := os.Chdir(cwd)
687+
if err != nil {
688+
// panic since this could affect other tests that haven't yet run
689+
panic(fmt.Sprintf("Couldn't go back to the original working directory: %v", err))
690+
}
691+
}()
692+
693+
err = os.Chdir(baseDirectory)
694+
if err != nil {
695+
t.Fatalf("Failed to change the working directory to %s: %v", baseDirectory, err)
696+
}
697+
698+
manifestPath := path.Join(baseDirectory, "configmap.yaml")
699+
yamlContent := "---\nkind: ConfigMap"
700+
err = ioutil.WriteFile(manifestPath, []byte(yamlContent), 0o666)
701+
if err != nil {
702+
t.Fatalf("Failed to write %s", manifestPath)
703+
}
704+
705+
tests := []struct {
706+
ManifestPath string
707+
ExpectedErrMsg string
708+
}{
709+
{manifestPath, ""},
710+
{"configmap.yaml", ""},
711+
{
712+
"../../../../temp.yaml",
713+
"the manifest path ../../../../temp.yaml is not in the same directory tree as the kustomization.yaml file",
714+
},
715+
{
716+
"..",
717+
"the manifest path .. is not in the same directory tree as the kustomization.yaml file",
718+
},
719+
{
720+
"/super/secret",
721+
"the manifest path /super/secret is not in the same directory tree as the kustomization.yaml file",
722+
},
723+
}
724+
725+
for _, test := range tests {
726+
test := test
727+
// nolint: paralleltest
728+
t.Run(
729+
"manifestPath="+test.ManifestPath,
730+
func(t *testing.T) {
731+
err := verifyManifestPath(baseDirectory, test.ManifestPath)
732+
if err == nil {
733+
assertEqual(t, "", test.ExpectedErrMsg)
734+
} else {
735+
assertEqual(t, err.Error(), test.ExpectedErrMsg)
736+
}
737+
},
738+
)
739+
}
740+
}

0 commit comments

Comments
 (0)