Skip to content

Commit 2392879

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 5b1b18e commit 2392879

4 files changed

Lines changed: 251 additions & 37 deletions

File tree

pkg/parse/parse.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,16 +1404,19 @@ func ContainerIgnoreFile(contextDir, path string, containerFiles []string) ([]st
14041404
return excludes, path, err
14051405
}
14061406
// If path was not supplied give priority to `<containerfile>.containerignore` first.
1407+
// Use securejoin.SecureJoin to prevent symlinks from escaping the context directory.
14071408
for _, containerfile := range containerFiles {
1408-
if !filepath.IsAbs(containerfile) {
1409-
containerfile = filepath.Join(contextDir, containerfile)
1410-
}
1409+
base := filepath.Base(containerfile)
14111410
containerfileIgnore := ""
1412-
if err := fileutils.Exists(containerfile + ".containerignore"); err == nil {
1413-
containerfileIgnore = containerfile + ".containerignore"
1414-
}
1415-
if err := fileutils.Exists(containerfile + ".dockerignore"); err == nil {
1416-
containerfileIgnore = containerfile + ".dockerignore"
1411+
for _, suffix := range []string{".containerignore", ".dockerignore"} {
1412+
candidate, err := securejoin.SecureJoin(contextDir, base+suffix)
1413+
if err != nil {
1414+
continue
1415+
}
1416+
if err := fileutils.Exists(candidate); err == nil {
1417+
containerfileIgnore = candidate
1418+
break
1419+
}
14171420
}
14181421
if containerfileIgnore != "" {
14191422
excludes, err := imagebuilder.ParseIgnore(containerfileIgnore)

pkg/util/util.go

Lines changed: 80 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,42 +41,97 @@ 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 target stays inside
48+
// the build context directory, matching docker build. Symlinks that resolve outside the
49+
// context are ignored so discovery fails as if the file were missing.
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+
foundCtrFile, err = discoverContainerfileCandidate(path, ctrfile)
66+
if err == nil {
67+
return foundCtrFile, nil
68+
}
69+
if !os.IsNotExist(err) {
70+
return "", err
6671
}
6772
}
73+
return "", fmt.Errorf("cannot find Containerfile or Dockerfile in context directory")
74+
75+
case target.Mode().IsRegular():
76+
return path, nil
6877

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)
78+
case target.Mode()&os.ModeSymlink != 0:
79+
return discoverContainerfileCandidate(filepath.Dir(path), path)
80+
81+
default:
82+
return "", fmt.Errorf("assumed Containerfile %q is not a file", path)
83+
}
84+
}
85+
86+
func discoverContainerfileCandidate(contextDir, ctrfile string) (string, error) {
87+
file, err := os.Lstat(ctrfile)
88+
if err != nil {
89+
return "", err
90+
}
91+
92+
if file.Mode()&os.ModeSymlink != 0 {
93+
inContext, err := containerfileSymlinkInContext(contextDir, ctrfile)
94+
if err != nil {
95+
return "", err
7496
}
97+
if !inContext {
98+
return "", os.ErrNotExist
99+
}
100+
file, err = os.Stat(ctrfile)
101+
if err != nil {
102+
return "", err
103+
}
104+
}
105+
106+
if file.Mode().IsRegular() {
107+
return ctrfile, nil
108+
}
109+
110+
return "", fmt.Errorf("assumed Containerfile %q is not a file", ctrfile)
111+
}
112+
113+
func containerfileSymlinkInContext(contextDir, ctrfile string) (bool, error) {
114+
contextDir, err := filepath.EvalSymlinks(contextDir)
115+
if err != nil {
116+
return false, err
117+
}
118+
119+
resolved, err := filepath.EvalSymlinks(ctrfile)
120+
if err != nil {
121+
if os.IsNotExist(err) {
122+
return false, nil
123+
}
124+
return false, err
125+
}
126+
127+
rel, err := filepath.Rel(contextDir, resolved)
128+
if err != nil {
129+
return false, err
130+
}
75131

76-
case mode.IsRegular():
77-
// If the context dir is a file, we assume this as Containerfile
78-
foundCtrFile = path
132+
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
133+
return false, nil
79134
}
80135

81-
return foundCtrFile, nil
136+
return true, nil
82137
}

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
}

tests/bud.bats

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8294,6 +8294,122 @@ srv.serve_forever()
82948294
assert "$output" = "ORIGINAL_CONTENT"
82958295
}
82968296

8297+
# https://github.com/podman-container-tools/buildah/issues/6861
8298+
@test "bud with local context symlinked Containerfile outside context" {
8299+
_prefetch alpine
8300+
8301+
local secretfile=${TEST_SCRATCH_DIR}/secretfile
8302+
cat > ${secretfile} << _EOF
8303+
FROM alpine
8304+
RUN echo SECRETHOSTCONTENT
8305+
_EOF
8306+
8307+
local contextdir=${TEST_SCRATCH_DIR}/dir2
8308+
mkdir -p ${contextdir}
8309+
ln -s ${secretfile} ${contextdir}/Containerfile
8310+
8311+
run_buildah 125 build $WITH_POLICY_JSON ${contextdir}
8312+
assert "$output" !~ "SECRETHOSTCONTENT"
8313+
}
8314+
8315+
# https://github.com/podman-container-tools/buildah/issues/6861
8316+
@test "bud with local context symlinked Containerfile within context" {
8317+
_prefetch alpine
8318+
8319+
local contextdir=${TEST_SCRATCH_DIR}/context
8320+
mkdir -p ${contextdir}/subdir
8321+
cat > ${contextdir}/subdir/Containerfile.real << _EOF
8322+
FROM alpine
8323+
RUN echo symlink-within-context-works
8324+
_EOF
8325+
ln -s subdir/Containerfile.real ${contextdir}/Containerfile
8326+
8327+
run_buildah build $WITH_POLICY_JSON ${contextdir}
8328+
assert "$output" =~ "symlink-within-context-works"
8329+
}
8330+
8331+
# https://github.com/podman-container-tools/buildah/issues/6861
8332+
@test "bud with stdin tar context rejects symlinked Dockerfile pointing outside temp dir" {
8333+
local targetfile=${TEST_SCRATCH_DIR}/targetfile
8334+
echo "SECRET_CONTENT" > ${targetfile}
8335+
8336+
local tarsrc=${TEST_SCRATCH_DIR}/tarsrc
8337+
mkdir -p ${tarsrc}
8338+
ln -s ${targetfile} ${tarsrc}/Dockerfile
8339+
local context_tar=${TEST_SCRATCH_DIR}/context.tar
8340+
tar -cf ${context_tar} -C ${tarsrc} Dockerfile
8341+
8342+
run_buildah 125 build $WITH_POLICY_JSON - < ${context_tar}
8343+
assert "$output" =~ "cannot find Containerfile or Dockerfile"
8344+
assert "$output" !~ "SECRET_CONTENT"
8345+
8346+
}
8347+
8348+
# https://github.com/podman-container-tools/buildah/issues/6861
8349+
@test "bud with http tar context rejects symlinked Dockerfile pointing outside temp dir" {
8350+
local targetfile=${TEST_SCRATCH_DIR}/targetfile
8351+
echo "SECRET_CONTENT" > ${targetfile}
8352+
8353+
local tarsrc=${TEST_SCRATCH_DIR}/tarsrc
8354+
mkdir -p ${tarsrc}
8355+
ln -s ${targetfile} ${tarsrc}/Dockerfile
8356+
local contentdir=${TEST_SCRATCH_DIR}/content
8357+
mkdir -p ${contentdir}
8358+
tar -cf ${contentdir}/context.tar -C ${tarsrc} Dockerfile
8359+
starthttpd ${contentdir}
8360+
8361+
run_buildah 125 build $WITH_POLICY_JSON http://0.0.0.0:${HTTP_SERVER_PORT}/context.tar
8362+
assert "$output" =~ "cannot find Containerfile or Dockerfile"
8363+
assert "$output" !~ "SECRET_CONTENT"
8364+
}
8365+
8366+
# https://github.com/podman-container-tools/buildah/issues/6861
8367+
# https://github.com/podman-container-tools/podman/issues/28749
8368+
@test "bud does not follow symlinked dockerignore outside context" {
8369+
_prefetch alpine
8370+
8371+
# dir/
8372+
# ign <- ignore file outside context (excludes "file")
8373+
# context/
8374+
# Dockerfile
8375+
# file
8376+
# Dockerfile.dockerignore -> ../ign (relative symlink escaping context)
8377+
local dir=${TEST_SCRATCH_DIR}
8378+
local contextdir=${dir}/context
8379+
mkdir -p ${contextdir}
8380+
echo "file" > ${dir}/ign
8381+
8382+
cat > ${contextdir}/Dockerfile << _EOF
8383+
FROM alpine
8384+
COPY file /dir/
8385+
RUN test -f /dir/file
8386+
_EOF
8387+
touch ${contextdir}/file
8388+
(cd ${contextdir} && ln -s ../ign Dockerfile.dockerignore)
8389+
8390+
run_buildah build $WITH_POLICY_JSON ${contextdir}
8391+
}
8392+
8393+
# https://github.com/podman-container-tools/buildah/issues/6861
8394+
# https://github.com/podman-container-tools/podman/issues/28749
8395+
@test "bud follows symlinked containerignore within context" {
8396+
_prefetch alpine
8397+
8398+
local contextdir=${TEST_SCRATCH_DIR}/context
8399+
mkdir -p ${contextdir}/conf
8400+
echo "file" > ${contextdir}/conf/ignore-rules
8401+
8402+
cat > ${contextdir}/Containerfile << _EOF
8403+
FROM alpine
8404+
COPY * /dir/
8405+
RUN test ! -f /dir/file
8406+
_EOF
8407+
touch ${contextdir}/file
8408+
ln -s conf/ignore-rules ${contextdir}/Containerfile.containerignore
8409+
8410+
run_buildah build $WITH_POLICY_JSON ${contextdir}
8411+
}
8412+
82978413
@test "build-validates-bind-bind-propagation" {
82988414
_prefetch alpine
82998415

0 commit comments

Comments
 (0)