Skip to content

Commit b0c55af

Browse files
committed
Reject symlinked Containerfile and ignore files escaping build context
Fixes: #6861 Fixes: podman-container-tools/podman#28749 Signed-off-by: Jan Rodák <hony.com@seznam.cz>
1 parent bb54d94 commit b0c55af

4 files changed

Lines changed: 371 additions & 54 deletions

File tree

pkg/parse/parse.go

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,37 +1422,66 @@ func ContainerIgnoreFile(contextDir, path string, containerFiles []string) ([]st
14221422
excludes, err := imagebuilder.ParseIgnore(path)
14231423
return excludes, path, err
14241424
}
1425-
// If path was not supplied give priority to `<containerfile>.containerignore` first.
1425+
// If path was not supplied, look for `<containerfile>.dockerignore` and
1426+
// `<containerfile>.containerignore`. When both exist, `.containerignore` wins.
1427+
// securejoin confines lookups with RESOLVE_IN_ROOT semantics, matching
1428+
// Docker BuildKit behavior. When the containerfile is inside contextDir
1429+
// we resolve relative to contextDir; when it is outside (e.g.
1430+
// overlay-mounted context or -f pointing elsewhere) we resolve relative
1431+
// to the containerfile's parent directory.
14261432
for _, containerfile := range containerFiles {
14271433
if !filepath.IsAbs(containerfile) {
14281434
containerfile = filepath.Join(contextDir, containerfile)
14291435
}
1430-
containerfileIgnore := ""
1431-
if err := fileutils.Exists(containerfile + ".containerignore"); err == nil {
1432-
containerfileIgnore = containerfile + ".containerignore"
1436+
cleanPath := filepath.Clean(containerfile)
1437+
relPath, relErr := filepath.Rel(contextDir, cleanPath)
1438+
insideContext := relErr == nil && relPath != ".." && !strings.HasPrefix(relPath, ".."+string(filepath.Separator))
1439+
var rootDir, baseName string
1440+
if insideContext {
1441+
rootDir = contextDir
1442+
baseName = relPath
1443+
} else {
1444+
rootDir = filepath.Dir(cleanPath)
1445+
baseName = filepath.Base(cleanPath)
14331446
}
1434-
if err := fileutils.Exists(containerfile + ".dockerignore"); err == nil {
1435-
containerfileIgnore = containerfile + ".dockerignore"
1447+
excludes, resolved, err := findIgnoreFile(rootDir, baseName+".dockerignore", baseName+".containerignore")
1448+
if err != nil {
1449+
return nil, "", err
14361450
}
1437-
if containerfileIgnore != "" {
1438-
excludes, err := imagebuilder.ParseIgnore(containerfileIgnore)
1439-
return excludes, containerfileIgnore, err
1451+
if resolved != "" {
1452+
return excludes, resolved, nil
14401453
}
14411454
}
1442-
path, symlinkErr := securejoin.SecureJoin(contextDir, ".containerignore")
1443-
if symlinkErr != nil {
1444-
return nil, "", symlinkErr
1455+
excludes, resolved, err := findIgnoreFile(contextDir, ".dockerignore", ".containerignore")
1456+
if err != nil {
1457+
return nil, "", err
14451458
}
1446-
excludes, err := imagebuilder.ParseIgnore(path)
1447-
if errors.Is(err, os.ErrNotExist) {
1448-
path, symlinkErr = securejoin.SecureJoin(contextDir, ".dockerignore")
1449-
if symlinkErr != nil {
1450-
return nil, "", symlinkErr
1451-
}
1452-
excludes, err = imagebuilder.ParseIgnore(path)
1459+
if resolved != "" {
1460+
return excludes, resolved, nil
14531461
}
1454-
if errors.Is(err, os.ErrNotExist) {
1455-
return excludes, "", nil
1462+
return nil, "", nil
1463+
}
1464+
1465+
// findIgnoreFile tries each candidate name resolved under rootDir using
1466+
// securejoin (RESOLVE_IN_ROOT semantics). When both exist the last one wins.
1467+
func findIgnoreFile(rootDir string, candidates ...string) ([]string, string, error) {
1468+
var excludes []string
1469+
var matched string
1470+
for _, name := range candidates {
1471+
resolved, err := securejoin.SecureJoin(rootDir, name)
1472+
if err != nil {
1473+
continue
1474+
}
1475+
f, err := os.Open(resolved)
1476+
if err != nil {
1477+
continue
1478+
}
1479+
excludes, err = imagebuilder.ParseIgnoreReader(f)
1480+
f.Close()
1481+
if err != nil {
1482+
return nil, "", err
1483+
}
1484+
matched = resolved
14561485
}
1457-
return excludes, path, err
1486+
return excludes, matched, nil
14581487
}

pkg/util/util.go

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package util //nolint:revive,nolintlint
22

33
import (
44
"fmt"
5+
"io/fs"
56
"os"
67
"path/filepath"
78
"strings"
89

10+
securejoin "github.com/cyphar/filepath-securejoin"
911
"go.podman.io/buildah/pkg/parse"
1012
)
1113

@@ -41,42 +43,68 @@ func MirrorToTempFileIfPathIsDescriptor(file string) (string, bool) {
4143
}
4244

4345
// DiscoverContainerfile tries to find a Containerfile or a Dockerfile within the provided `path`.
46+
// The path may be a directory (in which case Containerfile/Dockerfile is searched inside it)
47+
// or a direct path to a container file.
48+
//
49+
// Symlinked Containerfile/Dockerfile entries are only used when their real
50+
// target stays inside the build context directory. Symlinks that resolve
51+
// outside the context or are dangling are skipped.
4452
func DiscoverContainerfile(path string) (foundCtrFile string, err error) {
45-
// Test for existence of the file
46-
target, err := os.Stat(path)
53+
path, err = filepath.Abs(path)
4754
if err != nil {
4855
return "", fmt.Errorf("discovering Containerfile: %w", err)
4956
}
5057

51-
switch mode := target.Mode(); {
52-
case mode.IsDir():
53-
// If the path is a real directory, we assume a Containerfile or a Dockerfile within it
54-
ctrfile := filepath.Join(path, "Containerfile")
55-
56-
// Test for existence of the Containerfile file
57-
file, err := os.Stat(ctrfile)
58-
if err != nil {
59-
// See if we have a Dockerfile within it
60-
ctrfile = filepath.Join(path, "Dockerfile")
58+
target, err := os.Lstat(path)
59+
if err != nil {
60+
return "", fmt.Errorf("discovering Containerfile: %w", err)
61+
}
6162

62-
// Test for existence of the Dockerfile file
63-
file, err = os.Stat(ctrfile)
64-
if err != nil {
65-
return "", fmt.Errorf("cannot find Containerfile or Dockerfile in context directory: %w", err)
63+
switch {
64+
case target.IsDir():
65+
for _, name := range []string{"Containerfile", "Dockerfile"} {
66+
ctrfile := filepath.Join(path, name)
67+
if resolved, ok := isRegularFileInContext(path, ctrfile); ok {
68+
return resolved, nil
6669
}
6770
}
71+
return "", fmt.Errorf("cannot find Containerfile or Dockerfile in context directory: %w", fs.ErrNotExist)
6872

69-
// The file exists, now verify the correct mode
70-
if mode := file.Mode(); mode.IsRegular() {
71-
foundCtrFile = ctrfile
72-
} else {
73-
return "", fmt.Errorf("assumed Containerfile %q is not a file", ctrfile)
73+
case target.Mode().IsRegular():
74+
return path, nil
75+
76+
case target.Mode()&os.ModeSymlink != 0:
77+
if resolved, ok := isRegularFileInContext(filepath.Dir(path), path); ok {
78+
return resolved, nil
7479
}
80+
return "", fmt.Errorf("assumed Containerfile %q is not a file", path)
7581

76-
case mode.IsRegular():
77-
// If the context dir is a file, we assume this as Containerfile
78-
foundCtrFile = path
82+
default:
83+
return "", fmt.Errorf("assumed Containerfile %q is not a file", path)
7984
}
85+
}
8086

81-
return foundCtrFile, nil
87+
// isRegularFileInContext checks whether path resolves to a regular file
88+
// inside contextDir using RESOLVE_IN_ROOT semantics (securejoin.SecureJoin):
89+
// ".." components are clamped to the root and absolute symlink targets are
90+
// re-rooted under contextDir. This matches Docker BuildKit's behavior.
91+
//
92+
// On success it returns the resolved host path.
93+
func isRegularFileInContext(contextDir, path string) (string, bool) {
94+
name, err := filepath.Rel(contextDir, path)
95+
if err != nil {
96+
return "", false
97+
}
98+
resolved, err := securejoin.SecureJoin(contextDir, name)
99+
if err != nil {
100+
return "", false
101+
}
102+
fi, err := os.Stat(resolved)
103+
if err != nil {
104+
return "", false
105+
}
106+
if !fi.Mode().IsRegular() {
107+
return "", false
108+
}
109+
return resolved, true
82110
}

pkg/util/util_test.go

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
package util //nolint:revive,nolintlint
22

33
import (
4+
"os"
5+
"path/filepath"
46
"testing"
57

68
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
710
)
811

12+
func absPath(t *testing.T, rel string) string {
13+
t.Helper()
14+
p, err := filepath.Abs(rel)
15+
require.NoError(t, err)
16+
return p
17+
}
18+
919
func TestDiscoverContainerfile(t *testing.T) {
1020
t.Parallel()
1121
_, err := DiscoverContainerfile("./bogus")
@@ -16,17 +26,88 @@ func TestDiscoverContainerfile(t *testing.T) {
1626

1727
name, err := DiscoverContainerfile("test/test1/Dockerfile")
1828
assert.Nil(t, err)
19-
assert.Equal(t, name, "test/test1/Dockerfile")
29+
assert.Equal(t, absPath(t, "test/test1/Dockerfile"), name)
2030

2131
name, err = DiscoverContainerfile("test/test1/Containerfile")
2232
assert.Nil(t, err)
23-
assert.Equal(t, name, "test/test1/Containerfile")
33+
assert.Equal(t, absPath(t, "test/test1/Containerfile"), name)
2434

2535
name, err = DiscoverContainerfile("test/test1")
2636
assert.Nil(t, err)
27-
assert.Equal(t, name, "test/test1/Containerfile")
37+
assert.Equal(t, absPath(t, "test/test1/Containerfile"), name)
2838

2939
name, err = DiscoverContainerfile("test/test2")
3040
assert.Nil(t, err)
31-
assert.Equal(t, name, "test/test2/Dockerfile")
41+
assert.Equal(t, absPath(t, "test/test2/Dockerfile"), name)
42+
}
43+
44+
func TestDiscoverContainerfileRejectsSymlinkOutsideContext(t *testing.T) {
45+
t.Parallel()
46+
tmpDir := t.TempDir()
47+
48+
secretFile := filepath.Join(tmpDir, "secret-Containerfile")
49+
require.NoError(t, os.WriteFile(secretFile, []byte("FROM scratch\n"), 0o644))
50+
51+
contextDir := filepath.Join(tmpDir, "context")
52+
require.NoError(t, os.Mkdir(contextDir, 0o755))
53+
require.NoError(t, os.Symlink(secretFile, filepath.Join(contextDir, "Containerfile")))
54+
55+
_, err := DiscoverContainerfile(contextDir)
56+
assert.Error(t, err)
57+
assert.Contains(t, err.Error(), "cannot find Containerfile or Dockerfile")
58+
}
59+
60+
func TestDiscoverContainerfileAcceptsSymlinkInsideContext(t *testing.T) {
61+
t.Parallel()
62+
contextDir := t.TempDir()
63+
64+
subdir := filepath.Join(contextDir, "subdir")
65+
require.NoError(t, os.Mkdir(subdir, 0o755))
66+
require.NoError(t, os.WriteFile(filepath.Join(subdir, "Containerfile.real"), []byte("FROM scratch\n"), 0o644))
67+
require.NoError(t, os.Symlink(filepath.Join("subdir", "Containerfile.real"), filepath.Join(contextDir, "Containerfile")))
68+
69+
name, err := DiscoverContainerfile(contextDir)
70+
require.NoError(t, err)
71+
assert.Equal(t, filepath.Join(contextDir, "subdir", "Containerfile.real"), name)
72+
}
73+
74+
func TestDiscoverContainerfileAcceptsEscapeClampedToRoot(t *testing.T) {
75+
t.Parallel()
76+
tmpDir := t.TempDir()
77+
78+
contextDir := filepath.Join(tmpDir, "context")
79+
require.NoError(t, os.Mkdir(contextDir, 0o755))
80+
require.NoError(t, os.WriteFile(filepath.Join(contextDir, "file"), []byte("FROM scratch\n"), 0o644))
81+
require.NoError(t, os.Symlink("../file", filepath.Join(contextDir, "Containerfile")))
82+
83+
name, err := DiscoverContainerfile(contextDir)
84+
require.NoError(t, err)
85+
assert.Equal(t, filepath.Join(contextDir, "file"), name)
86+
}
87+
88+
func TestDiscoverContainerfileAcceptsAbsoluteSymlinkRerooted(t *testing.T) {
89+
t.Parallel()
90+
contextDir := t.TempDir()
91+
92+
subdir := filepath.Join(contextDir, "subdirectory")
93+
require.NoError(t, os.Mkdir(subdir, 0o755))
94+
require.NoError(t, os.WriteFile(filepath.Join(subdir, "real.file"), []byte("FROM scratch\n"), 0o644))
95+
require.NoError(t, os.Symlink("/subdirectory/real.file", filepath.Join(contextDir, "Containerfile")))
96+
97+
name, err := DiscoverContainerfile(contextDir)
98+
require.NoError(t, err)
99+
assert.Equal(t, filepath.Join(contextDir, "subdirectory", "real.file"), name)
100+
}
101+
102+
func TestDiscoverContainerfileRejectsNonExistentClampedTarget(t *testing.T) {
103+
t.Parallel()
104+
tmpDir := t.TempDir()
105+
106+
contextDir := filepath.Join(tmpDir, "context")
107+
require.NoError(t, os.Mkdir(contextDir, 0o755))
108+
require.NoError(t, os.Symlink("../nonexistent", filepath.Join(contextDir, "Containerfile")))
109+
110+
_, err := DiscoverContainerfile(contextDir)
111+
assert.Error(t, err)
112+
assert.Contains(t, err.Error(), "cannot find Containerfile or Dockerfile")
32113
}

0 commit comments

Comments
 (0)