@@ -21,6 +21,7 @@ package snap
2121
2222import (
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.
82225func 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