|
4 | 4 | "os" |
5 | 5 | "os/exec" |
6 | 6 | "path/filepath" |
| 7 | + "runtime" |
7 | 8 | "strings" |
8 | 9 | "testing" |
9 | 10 | ) |
@@ -146,6 +147,33 @@ func TestBuildRunArgs_ForbiddenVolumeMountRejected(t *testing.T) { |
146 | 147 | } |
147 | 148 | } |
148 | 149 |
|
| 150 | +// TestBuildRunArgs_InWorkdirMountUnderSystemRootAllowed guards against the |
| 151 | +// regression where the broad system-root forbidden prefixes (/home, /root, |
| 152 | +// /var, /run) rejected every legitimate in-workdir mount on a typical Linux |
| 153 | +// host, where the working directory itself lives under /home/<user>. |
| 154 | +func TestBuildRunArgs_InWorkdirMountUnderSystemRootAllowed(t *testing.T) { |
| 155 | + for _, workdir := range []string{"/home/alice/project", "/root/project", "/var/lib/app", "/run/app"} { |
| 156 | + mount := workdir + "/data:/container/data" |
| 157 | + args := BuildRunArgs(Config{Volumes: []string{mount}}, "odek-test", workdir, "alpine:latest") |
| 158 | + if !contains(args, mount) { |
| 159 | + t.Errorf("in-workdir mount under system root should be allowed for workdir %q\nargs: %v", workdir, args) |
| 160 | + } |
| 161 | + } |
| 162 | +} |
| 163 | + |
| 164 | +// TestBuildRunArgs_SystemRootMountOutsideWorkdirStillRejected confirms the |
| 165 | +// system-root protection still fires when the path is NOT inside the workdir. |
| 166 | +func TestBuildRunArgs_SystemRootMountOutsideWorkdirStillRejected(t *testing.T) { |
| 167 | + // workdir is /, so /etc/secret is lexically "under" the workdir and passes |
| 168 | + // confinement, but must still be rejected by the /etc forbidden prefix. |
| 169 | + args := BuildRunArgs(Config{Volumes: []string{"/etc/secret:/container/secret"}}, "odek-test", "/", "alpine:latest") |
| 170 | + for i, a := range args { |
| 171 | + if a == "-v" && i+1 < len(args) && strings.HasPrefix(args[i+1], "/etc/secret:") { |
| 172 | + t.Errorf("mount into /etc should be rejected even when workdir is /, found %q", args[i+1]) |
| 173 | + } |
| 174 | + } |
| 175 | +} |
| 176 | + |
149 | 177 | func TestBuildRunArgs_VolumeOutsideWorkdirRejected(t *testing.T) { |
150 | 178 | args := BuildRunArgs(Config{ |
151 | 179 | Volumes: []string{"/tmp:/container/tmp"}, |
@@ -179,6 +207,32 @@ func TestBuildRunArgs_DockerSocketRejected(t *testing.T) { |
179 | 207 | } |
180 | 208 | } |
181 | 209 |
|
| 210 | +// TestBuildRunArgs_IntermediateSymlinkEscapeRejected verifies that a mount |
| 211 | +// whose final component is a regular file (passing the Lstat check) but whose |
| 212 | +// parent is a symlink pointing outside the working directory is rejected. |
| 213 | +func TestBuildRunArgs_IntermediateSymlinkEscapeRejected(t *testing.T) { |
| 214 | + if runtime.GOOS == "windows" { |
| 215 | + t.Skip("symlink tests skipped on windows") |
| 216 | + } |
| 217 | + workdir := t.TempDir() |
| 218 | + outside := t.TempDir() |
| 219 | + if err := os.WriteFile(filepath.Join(outside, "secret"), []byte("x"), 0644); err != nil { |
| 220 | + t.Fatal(err) |
| 221 | + } |
| 222 | + // workdir/link -> outside (a directory symlink that escapes the workdir). |
| 223 | + if err := os.Symlink(outside, filepath.Join(workdir, "link")); err != nil { |
| 224 | + t.Fatal(err) |
| 225 | + } |
| 226 | + |
| 227 | + mount := filepath.Join(workdir, "link", "secret") + ":/container/secret" |
| 228 | + args := BuildRunArgs(Config{Volumes: []string{mount}}, "odek-test", workdir, "alpine:latest") |
| 229 | + for i, a := range args { |
| 230 | + if a == "-v" && i+1 < len(args) && strings.Contains(args[i+1], "secret") { |
| 231 | + t.Errorf("mount traversing an intermediate symlink out of workdir should be rejected, found %q", args[i+1]) |
| 232 | + } |
| 233 | + } |
| 234 | +} |
| 235 | + |
182 | 236 | func TestBuildRunArgs_RelativeVolumeResolvedUnderWorkdir(t *testing.T) { |
183 | 237 | args := BuildRunArgs(Config{ |
184 | 238 | Volumes: []string{"extra:/container/extra"}, |
|
0 commit comments