@@ -21,6 +21,7 @@ package snap
21
21
22
22
import (
23
23
"errors"
24
+ "fmt"
24
25
"io"
25
26
"os"
26
27
"path/filepath"
@@ -44,6 +45,12 @@ type Container interface {
44
45
// ReadFile returns the content of a single file from the snap.
45
46
ReadFile (relative string ) ([]byte , error )
46
47
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
+
47
54
// Walk is like filepath.Walk, without the ordering guarantee.
48
55
Walk (relative string , walkFn filepath.WalkFunc ) error
49
56
@@ -78,6 +85,142 @@ var (
78
85
ErrMissingPaths = errors .New ("snap is unusable due to missing files" )
79
86
)
80
87
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
+
81
224
// ValidateComponentContainer does a minimal quick check on a snap component container.
82
225
func ValidateComponentContainer (c Container , contName string , logf func (format string , v ... interface {})) error {
83
226
needsrx := map [string ]bool {
@@ -196,20 +339,32 @@ func validateContainer(c Container, needsrx, needsx, needsr, needsf, noskipd map
196
339
return nil
197
340
}
198
341
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 () {
200
354
if mode .Perm ()& 0555 != 0555 {
201
355
logf ("in %s %q: %q should be world-readable and executable, and isn't: %s" , contType , name , path , mode )
202
356
hasBadModes = true
203
357
}
204
358
} 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 {
213
368
logf ("in %s %q: %q should be a regular file (or a symlink) and isn't" , contType , name , path )
214
369
hasBadModes = true
215
370
}
0 commit comments