|
1 | 1 | package security |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "io" |
| 5 | + "os" |
| 6 | + "path/filepath" |
| 7 | + "runtime" |
4 | 8 | "testing" |
5 | 9 |
|
6 | 10 | "github.com/stretchr/testify/assert" |
7 | 11 | "github.com/stretchr/testify/require" |
8 | 12 | ) |
9 | 13 |
|
10 | 14 | func TestEnforceToCurrentRoot(t *testing.T) { |
11 | | - cleanDir, err := EnforceToCurrentRoot("/home/argo/helmapp/", "/home/argo/helmapp/values.yaml") |
12 | | - require.NoError(t, err) |
13 | | - assert.Equal(t, "/home/argo/helmapp/values.yaml", cleanDir) |
14 | | - |
15 | | - // File is outside current working directory |
16 | | - _, err = EnforceToCurrentRoot("/home/argo/helmapp/", "/home/values.yaml") |
17 | | - require.Error(t, err) |
18 | | - |
19 | | - // File is outside current working directory |
20 | | - _, err = EnforceToCurrentRoot("/home/argo/helmapp/", "/home/argo/helmapp/../differentapp/values.yaml") |
21 | | - require.Error(t, err) |
22 | | - |
23 | | - // Goes back and forth, but still legal |
24 | | - cleanDir, err = EnforceToCurrentRoot("/home/argo/helmapp/", "/home/argo/helmapp/../../argo/helmapp/values.yaml") |
25 | | - require.NoError(t, err) |
26 | | - assert.Equal(t, "/home/argo/helmapp/values.yaml", cleanDir) |
| 15 | + tmpDir := t.TempDir() |
| 16 | + |
| 17 | + // Setup: Create files with specific content so we can verify we opened the right one. |
| 18 | + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "values.yaml"), []byte("root-file"), 0o644)) |
| 19 | + require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "charts"), 0o755)) |
| 20 | + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "charts", "values.yaml"), []byte("subdir-file"), 0o644)) |
| 21 | + require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "a", "b", "c", "d"), 0o755)) |
| 22 | + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "a", "b", "c", "d", "values.yaml"), []byte("deep-file"), 0o644)) |
| 23 | + |
| 24 | + // Helper to verify we received a valid file handle pointing to the expected data. |
| 25 | + verifyFileContent := func(t *testing.T, f *os.File, expectedContent string) { |
| 26 | + t.Helper() |
| 27 | + defer f.Close() |
| 28 | + content, err := io.ReadAll(f) |
| 29 | + require.NoError(t, err) |
| 30 | + assert.Equal(t, expectedContent, string(content)) |
| 31 | + } |
| 32 | + |
| 33 | + t.Run("file directly in root", func(t *testing.T) { |
| 34 | + testFile := filepath.Join(tmpDir, "values.yaml") |
| 35 | + fileHandle, err := EnforceToCurrentRoot(tmpDir, testFile) |
| 36 | + require.NoError(t, err) |
| 37 | + require.NotNil(t, fileHandle) |
| 38 | + verifyFileContent(t, fileHandle, "root-file") |
| 39 | + }) |
| 40 | + |
| 41 | + t.Run("file in subdirectory", func(t *testing.T) { |
| 42 | + testFile := filepath.Join(tmpDir, "charts", "values.yaml") |
| 43 | + fileHandle, err := EnforceToCurrentRoot(tmpDir, testFile) |
| 44 | + require.NoError(t, err) |
| 45 | + require.NotNil(t, fileHandle) |
| 46 | + verifyFileContent(t, fileHandle, "subdir-file") |
| 47 | + }) |
| 48 | + |
| 49 | + t.Run("file outside current working directory", func(t *testing.T) { |
| 50 | + outsidePath := filepath.Join(filepath.Dir(tmpDir), "values.yaml") |
| 51 | + _, err := EnforceToCurrentRoot(tmpDir, outsidePath) |
| 52 | + require.Error(t, err) |
| 53 | + assert.Contains(t, err.Error(), "should be on or under current directory") |
| 54 | + }) |
| 55 | + |
| 56 | + t.Run("file escapes using parent directory notation", func(t *testing.T) { |
| 57 | + escapePath := filepath.Join(tmpDir, "..", "differentapp", "values.yaml") |
| 58 | + _, err := EnforceToCurrentRoot(tmpDir, escapePath) |
| 59 | + require.Error(t, err) |
| 60 | + assert.Contains(t, err.Error(), "should be on or under current directory") |
| 61 | + }) |
| 62 | + |
| 63 | + t.Run("goes back and forth but remains within root", func(t *testing.T) { |
| 64 | + complexPath := filepath.Join(tmpDir, "charts", "..", "values.yaml") |
| 65 | + fileHandle, err := EnforceToCurrentRoot(tmpDir, complexPath) |
| 66 | + require.NoError(t, err) |
| 67 | + require.NotNil(t, fileHandle) |
| 68 | + verifyFileContent(t, fileHandle, "root-file") |
| 69 | + }) |
| 70 | + |
| 71 | + t.Run("path equals root directory", func(t *testing.T) { |
| 72 | + // Opening the directory itself should succeed |
| 73 | + fileHandle, err := EnforceToCurrentRoot(tmpDir, tmpDir) |
| 74 | + require.NoError(t, err) |
| 75 | + require.NotNil(t, fileHandle) |
| 76 | + defer fileHandle.Close() |
| 77 | + |
| 78 | + info, err := fileHandle.Stat() |
| 79 | + require.NoError(t, err) |
| 80 | + assert.True(t, info.IsDir()) |
| 81 | + }) |
| 82 | + |
| 83 | + t.Run("complex path with multiple . and ..", func(t *testing.T) { |
| 84 | + complexPath := filepath.Join(tmpDir, ".", "charts", "..", "values.yaml") |
| 85 | + fileHandle, err := EnforceToCurrentRoot(tmpDir, complexPath) |
| 86 | + require.NoError(t, err) |
| 87 | + require.NotNil(t, fileHandle) |
| 88 | + verifyFileContent(t, fileHandle, "root-file") |
| 89 | + }) |
| 90 | + |
| 91 | + t.Run("root with trailing slash vs without", func(t *testing.T) { |
| 92 | + testFile := filepath.Join(tmpDir, "values.yaml") |
| 93 | + |
| 94 | + // 1. With Slash. |
| 95 | + rootWithSlash := tmpDir + string(filepath.Separator) |
| 96 | + f1, err1 := EnforceToCurrentRoot(rootWithSlash, testFile) |
| 97 | + require.NoError(t, err1) |
| 98 | + verifyFileContent(t, f1, "root-file") |
| 99 | + |
| 100 | + // 2. Without Slash. |
| 101 | + f2, err2 := EnforceToCurrentRoot(tmpDir, testFile) |
| 102 | + require.NoError(t, err2) |
| 103 | + verifyFileContent(t, f2, "root-file") |
| 104 | + }) |
| 105 | + |
| 106 | + t.Run("attempt to escape with multiple parent references", func(t *testing.T) { |
| 107 | + escapePath := filepath.Join(tmpDir, "..", "..", "..", "etc", "passwd") |
| 108 | + _, err := EnforceToCurrentRoot(tmpDir, escapePath) |
| 109 | + require.Error(t, err) |
| 110 | + }) |
| 111 | + |
| 112 | + t.Run("deep nested subdirectory", func(t *testing.T) { |
| 113 | + testFile := filepath.Join(tmpDir, "a", "b", "c", "d", "values.yaml") |
| 114 | + fileHandle, err := EnforceToCurrentRoot(tmpDir, testFile) |
| 115 | + require.NoError(t, err) |
| 116 | + require.NotNil(t, fileHandle) |
| 117 | + verifyFileContent(t, fileHandle, "deep-file") |
| 118 | + }) |
| 119 | +} |
| 120 | + |
| 121 | +func TestEnforceToCurrentRootEdgeCases(t *testing.T) { |
| 122 | + tmpDir := t.TempDir() |
| 123 | + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "values.yaml"), []byte("test"), 0o644)) |
| 124 | + |
| 125 | + t.Run("empty paths", func(t *testing.T) { |
| 126 | + _, err := EnforceToCurrentRoot("", "/some/path") |
| 127 | + require.Error(t, err) |
| 128 | + }) |
| 129 | + |
| 130 | + t.Run("relative root path", func(t *testing.T) { |
| 131 | + // EnforceToCurrentRoot expects absolute paths for the root (usually) |
| 132 | + // os.OpenRoot might work with relative paths, but filepath.Rel logic depends on clean comparisons. |
| 133 | + _, err := EnforceToCurrentRoot("relative/path", "/absolute/path") |
| 134 | + require.Error(t, err) |
| 135 | + }) |
| 136 | + |
| 137 | + t.Run("path with double slashes", func(t *testing.T) { |
| 138 | + doubleSlashPath := tmpDir + string(filepath.Separator) + string(filepath.Separator) + "values.yaml" |
| 139 | + f, err := EnforceToCurrentRoot(tmpDir, doubleSlashPath) |
| 140 | + require.NoError(t, err) |
| 141 | + defer f.Close() |
| 142 | + }) |
| 143 | + |
| 144 | + t.Run("non-existent file in valid directory", func(t *testing.T) { |
| 145 | + nonExistentPath := filepath.Join(tmpDir, "does-not-exist.yaml") |
| 146 | + _, err := EnforceToCurrentRoot(tmpDir, nonExistentPath) |
| 147 | + // This should fail because root.Open() checks if file exists. |
| 148 | + require.Error(t, err) |
| 149 | + }) |
| 150 | +} |
| 151 | + |
| 152 | +func TestEnforceToCurrentRootWindowsPaths(t *testing.T) { |
| 153 | + if runtime.GOOS != "windows" { |
| 154 | + t.Skip("Skipping Windows-specific tests on non-Windows platform") |
| 155 | + } |
| 156 | + tmpDir := t.TempDir() |
| 157 | + testFile := filepath.Join(tmpDir, "values.yaml") |
| 158 | + require.NoError(t, os.WriteFile(testFile, []byte("test"), 0o644)) |
| 159 | + |
| 160 | + t.Run("windows path in root", func(t *testing.T) { |
| 161 | + f, err := EnforceToCurrentRoot(tmpDir, testFile) |
| 162 | + require.NoError(t, err) |
| 163 | + defer f.Close() |
| 164 | + }) |
| 165 | + |
| 166 | + t.Run("windows path escape attempt", func(t *testing.T) { |
| 167 | + escapePath := filepath.Join(tmpDir, "..", "..", "Windows", "System32", "config") |
| 168 | + _, err := EnforceToCurrentRoot(tmpDir, escapePath) |
| 169 | + require.Error(t, err) |
| 170 | + }) |
27 | 171 | } |
0 commit comments