Skip to content

Commit b66fee8

Browse files
ZeyadYasserMeulengracht
authored andcommitted
many: container validation improvements
Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>
1 parent 2e61edf commit b66fee8

File tree

9 files changed

+801
-106
lines changed

9 files changed

+801
-106
lines changed

snap/container.go

+164-9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package snap
2121

2222
import (
2323
"errors"
24+
"fmt"
2425
"io"
2526
"os"
2627
"path/filepath"
@@ -44,6 +45,12 @@ type Container interface {
4445
// ReadFile returns the content of a single file from the snap.
4546
ReadFile(relative string) ([]byte, error)
4647

48+
// ReadLink returns the destination of the named symbolic link.
49+
ReadLink(relative string) (string, error)
50+
51+
// Lstat is like os.Lstat.
52+
Lstat(relative string) (os.FileInfo, error)
53+
4754
// Walk is like filepath.Walk, without the ordering guarantee.
4855
Walk(relative string, walkFn filepath.WalkFunc) error
4956

@@ -78,6 +85,142 @@ var (
7885
ErrMissingPaths = errors.New("snap is unusable due to missing files")
7986
)
8087

88+
type symlinkInfo struct {
89+
// target is the furthest target we could evaluate.
90+
target string
91+
// targetMode is the mode of the final symlink target.
92+
targetMode os.FileMode
93+
// naiveTarget is the first symlink target.
94+
naiveTarget string
95+
// isExternal determines if the symlink is considered external
96+
// relative to its container.
97+
isExternal bool
98+
}
99+
100+
// evalSymlink follows symlinks inside given container and returns
101+
// information about it's target.
102+
//
103+
// The symlink is followed inside the container until we cannot
104+
// continue further either due to absolute symlinks or symlinks
105+
// that escape the container.
106+
//
107+
// max depth reached?<------
108+
// /\ \
109+
// yes no \
110+
// / \ \
111+
// V V \
112+
// error path \
113+
// │ \
114+
// V \
115+
// read target \
116+
// │ \
117+
// V \
118+
// is absolute? \
119+
// /\ \
120+
// yes no \
121+
// / \ \
122+
// V V \
123+
// isExternal eval relative target \
124+
// + \ \
125+
// return target V \
126+
// escapes container? \
127+
// /\ \
128+
// yes no \
129+
// / \ |
130+
// V V |
131+
// isExternal is symlink? |
132+
// + /\ |
133+
// return target yes no │
134+
// / \ │
135+
// V V │
136+
// !isExternal path = target │
137+
// + \----------│
138+
// return target
139+
//
140+
func evalSymlink(c Container, path string) (symlinkInfo, error) {
141+
var naiveTarget string
142+
143+
const maxDepth = 10
144+
currentDepth := 0
145+
for currentDepth < maxDepth {
146+
currentDepth++
147+
target, err := c.ReadLink(path)
148+
if err != nil {
149+
return symlinkInfo{}, err
150+
}
151+
// record first symlink target
152+
if currentDepth == 1 {
153+
naiveTarget = target
154+
}
155+
156+
target = filepath.Clean(target)
157+
// don't follow absolute targets
158+
if filepath.IsAbs(target) {
159+
return symlinkInfo{target, os.FileMode(0), naiveTarget, true}, nil
160+
}
161+
162+
// evaluate target relative to symlink directory
163+
target = filepath.Join(filepath.Dir(path), target)
164+
165+
// target escapes container, cannot evaluate further, let's return
166+
if strings.Split(target, string(os.PathSeparator))[0] == ".." {
167+
return symlinkInfo{target, os.FileMode(0), naiveTarget, true}, nil
168+
}
169+
170+
info, err := c.Lstat(target)
171+
// cannot follow bad targets
172+
if err != nil {
173+
return symlinkInfo{}, err
174+
}
175+
176+
// non-symlink, let's return
177+
if info.Mode().Type() != os.ModeSymlink {
178+
return symlinkInfo{target, info.Mode(), naiveTarget, false}, nil
179+
}
180+
181+
// we have another symlink
182+
path = target
183+
}
184+
185+
return symlinkInfo{}, fmt.Errorf("too many levels of symbolic links")
186+
}
187+
188+
func shouldValidateSymlink(path string) bool {
189+
// we only check meta directory for now
190+
pathTokens := strings.Split(path, string(os.PathSeparator))
191+
if pathTokens[0] == "meta" {
192+
return true
193+
}
194+
return false
195+
}
196+
197+
func evalAndValidateSymlink(c Container, path string) (symlinkInfo, error) {
198+
pathTokens := strings.Split(path, string(os.PathSeparator))
199+
// check if meta directory is a symlink
200+
if len(pathTokens) == 1 && pathTokens[0] == "meta" {
201+
return symlinkInfo{}, fmt.Errorf("meta directory cannot be a symlink")
202+
}
203+
204+
info, err := evalSymlink(c, path)
205+
if err != nil {
206+
return symlinkInfo{}, err
207+
}
208+
209+
if info.isExternal {
210+
return symlinkInfo{}, fmt.Errorf("external symlink found: %s -> %s", path, info.naiveTarget)
211+
}
212+
213+
// symlinks like this don't look innocent
214+
badTargets := []string{".", "meta"}
215+
for _, badTarget := range badTargets {
216+
if info.target == badTarget {
217+
return symlinkInfo{}, fmt.Errorf("bad symlink found: %s -> %s", path, info.naiveTarget)
218+
}
219+
}
220+
221+
return info, nil
222+
}
223+
81224
// ValidateComponentContainer does a minimal quick check on a snap component container.
82225
func ValidateComponentContainer(c Container, contName string, logf func(format string, v ...interface{})) error {
83226
needsrx := map[string]bool{
@@ -196,20 +339,32 @@ func validateContainer(c Container, needsrx, needsx, needsr, needsf, noskipd map
196339
return nil
197340
}
198341

199-
if needsrx[path] || mode.IsDir() {
342+
if mode&os.ModeSymlink != 0 && shouldValidateSymlink(path) {
343+
symlinkInfo, err := evalAndValidateSymlink(c, path)
344+
if err != nil {
345+
logf("%s", err)
346+
hasBadModes = true
347+
} else {
348+
// use target mode for checks below
349+
mode = symlinkInfo.targetMode
350+
}
351+
}
352+
353+
if mode.IsDir() {
200354
if mode.Perm()&0555 != 0555 {
201355
logf("in %s %q: %q should be world-readable and executable, and isn't: %s", contType, name, path, mode)
202356
hasBadModes = true
203357
}
204358
} else {
205-
if needsf[path] {
206-
// this assumes that if it's a symlink it's OK. Arguably we
207-
// should instead follow the symlink. We'd have to expose
208-
// Lstat(), and guard against loops, and ... huge can of
209-
// worms, and as this validator is meant as a developer aid
210-
// more than anything else, not worth it IMHO (as I can't
211-
// imagine this happening by accident).
212-
if mode&(os.ModeDir|os.ModeNamedPipe|os.ModeSocket|os.ModeDevice) != 0 {
359+
if needsrx[path] {
360+
if mode.Perm()&0555 != 0555 {
361+
logf("in snap %q: %q should be world-readable and executable, and isn't: %s", name, path, mode)
362+
hasBadModes = true
363+
}
364+
}
365+
// XXX: do we need to match other directories?
366+
if needsf[path] || strings.HasPrefix(path, "meta/") {
367+
if mode&(os.ModeNamedPipe|os.ModeSocket|os.ModeDevice) != 0 {
213368
logf("in %s %q: %q should be a regular file (or a symlink) and isn't", contType, name, path)
214369
hasBadModes = true
215370
}

0 commit comments

Comments
 (0)