Skip to content

Commit 0f40b4f

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 6fe248a commit 0f40b4f

4 files changed

Lines changed: 302 additions & 55 deletions

File tree

pkg/parse/parse.go

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"unicode"
1818

1919
"github.com/containerd/platforms"
20-
securejoin "github.com/cyphar/filepath-securejoin"
2120
units "github.com/docker/go-units"
2221
specs "github.com/opencontainers/runtime-spec/specs-go"
2322
"github.com/opencontainers/selinux/go-selinux"
@@ -1419,37 +1418,67 @@ func ContainerIgnoreFile(contextDir, path string, containerFiles []string) ([]st
14191418
excludes, err := imagebuilder.ParseIgnore(path)
14201419
return excludes, path, err
14211420
}
1422-
// If path was not supplied give priority to `<containerfile>.containerignore` first.
1421+
// If path was not supplied, look for `<containerfile>.dockerignore` and
1422+
// `<containerfile>.containerignore`. When both exist, `.containerignore` wins.
14231423
for _, containerfile := range containerFiles {
14241424
if !filepath.IsAbs(containerfile) {
14251425
containerfile = filepath.Join(contextDir, containerfile)
14261426
}
1427-
containerfileIgnore := ""
1428-
if err := fileutils.Exists(containerfile + ".containerignore"); err == nil {
1429-
containerfileIgnore = containerfile + ".containerignore"
1427+
cleanPath := filepath.Clean(containerfile)
1428+
relPath, relErr := filepath.Rel(contextDir, cleanPath)
1429+
insideContext := relErr == nil && relPath != ".." && !strings.HasPrefix(relPath, ".."+string(filepath.Separator))
1430+
var rootDir, baseName string
1431+
if insideContext {
1432+
rootDir = contextDir
1433+
baseName = relPath
1434+
} else {
1435+
rootDir = filepath.Dir(cleanPath)
1436+
baseName = filepath.Base(cleanPath)
1437+
}
1438+
root, err := os.OpenRoot(rootDir)
1439+
if err != nil {
1440+
continue
14301441
}
1431-
if err := fileutils.Exists(containerfile + ".dockerignore"); err == nil {
1432-
containerfileIgnore = containerfile + ".dockerignore"
1442+
excludes, name, err := findIgnoreFile(root, baseName+".dockerignore", baseName+".containerignore")
1443+
root.Close()
1444+
if err != nil {
1445+
return nil, "", err
14331446
}
1434-
if containerfileIgnore != "" {
1435-
excludes, err := imagebuilder.ParseIgnore(containerfileIgnore)
1436-
return excludes, containerfileIgnore, err
1447+
if name != "" {
1448+
return excludes, filepath.Join(rootDir, name), nil
14371449
}
14381450
}
1439-
path, symlinkErr := securejoin.SecureJoin(contextDir, ".containerignore")
1440-
if symlinkErr != nil {
1441-
return nil, "", symlinkErr
1451+
root, err := os.OpenRoot(contextDir)
1452+
if err != nil {
1453+
return nil, "", err
14421454
}
1443-
excludes, err := imagebuilder.ParseIgnore(path)
1444-
if errors.Is(err, os.ErrNotExist) {
1445-
path, symlinkErr = securejoin.SecureJoin(contextDir, ".dockerignore")
1446-
if symlinkErr != nil {
1447-
return nil, "", symlinkErr
1448-
}
1449-
excludes, err = imagebuilder.ParseIgnore(path)
1455+
defer root.Close()
1456+
excludes, name, err := findIgnoreFile(root, ".dockerignore", ".containerignore")
1457+
if err != nil {
1458+
return nil, "", err
14501459
}
1451-
if errors.Is(err, os.ErrNotExist) {
1452-
return excludes, "", nil
1460+
if name != "" {
1461+
return excludes, filepath.Join(contextDir, name), nil
1462+
}
1463+
return nil, "", nil
1464+
}
1465+
1466+
// findIgnoreFile tries each candidate name inside root. When both exist the
1467+
// last one wins (i.e. .containerignore takes precedence over .dockerignore).
1468+
func findIgnoreFile(root *os.Root, candidates ...string) ([]string, string, error) {
1469+
var excludes []string
1470+
var matched string
1471+
for _, name := range candidates {
1472+
f, err := root.Open(name)
1473+
if err != nil {
1474+
continue
1475+
}
1476+
excludes, err = imagebuilder.ParseIgnoreReader(f)
1477+
f.Close()
1478+
if err != nil {
1479+
return nil, "", err
1480+
}
1481+
matched = name
14531482
}
1454-
return excludes, path, err
1483+
return excludes, matched, nil
14551484
}

pkg/util/util.go

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,42 +41,67 @@ func MirrorToTempFileIfPathIsDescriptor(file string) (string, bool) {
4141
}
4242

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

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")
56+
target, err := os.Lstat(path)
57+
if err != nil {
58+
return "", fmt.Errorf("discovering Containerfile: %w", err)
59+
}
6160

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)
61+
switch {
62+
case target.IsDir():
63+
for _, name := range []string{"Containerfile", "Dockerfile"} {
64+
ctrfile := filepath.Join(path, name)
65+
if isRegularFileInContext(path, ctrfile) {
66+
return ctrfile, nil
6667
}
6768
}
69+
return "", fmt.Errorf("cannot find Containerfile or Dockerfile in context directory")
70+
71+
case target.Mode().IsRegular():
72+
return path, nil
6873

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)
74+
case target.Mode()&os.ModeSymlink != 0:
75+
if isRegularFileInContext(filepath.Dir(path), path) {
76+
return path, nil
7477
}
78+
return "", fmt.Errorf("assumed Containerfile %q is not a file", path)
7579

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

81-
return foundCtrFile, nil
85+
// isRegularFileInContext returns true if path is a regular file (or a symlink
86+
// to one) whose real target is inside contextDir.
87+
// It uses os.Root to confine all lookups within contextDir, so symlinks
88+
// that escape the context at any point. Including intermediate components
89+
// and absolute targets are rejected.
90+
func isRegularFileInContext(contextDir, path string) bool {
91+
root, err := os.OpenRoot(contextDir)
92+
if err != nil {
93+
return false
94+
}
95+
defer root.Close()
96+
name, err := filepath.Rel(contextDir, path)
97+
if err != nil {
98+
return false
99+
}
100+
// Stat will follow the symlink and verify the target is
101+
// a regular file inside the root. If any hop escapes, Stat will error out.
102+
fi, err := root.Stat(name)
103+
if err != nil {
104+
return false
105+
}
106+
return fi.Mode().IsRegular()
82107
}

pkg/util/util_test.go

Lines changed: 44 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,47 @@ 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, "Containerfile"), name)
3272
}

0 commit comments

Comments
 (0)