Browse code

pkg/symlink: avoid following out of scope

Docker-DCO-1.1-Signed-off-by: Cristian Staretu <cristian.staretu@gmail.com> (github: unclejack)

unclejack authored on 2014/10/29 06:18:45
Showing 3 changed files
... ...
@@ -12,6 +12,12 @@ const maxLoopCounter = 100
12 12
 
13 13
 // FollowSymlink will follow an existing link and scope it to the root
14 14
 // path provided.
15
+// The role of this function is to return an absolute path in the root
16
+// or normalize to the root if the symlink leads to a path which is
17
+// outside of the root.
18
+// Errors encountered while attempting to follow the symlink in path
19
+// will be reported.
20
+// Normalizations to the root don't constitute errors.
15 21
 func FollowSymlinkInScope(link, root string) (string, error) {
16 22
 	root, err := filepath.Abs(root)
17 23
 	if err != nil {
... ...
@@ -60,25 +66,36 @@ func FollowSymlinkInScope(link, root string) (string, error) {
60 60
 				}
61 61
 				return "", err
62 62
 			}
63
-			if stat.Mode()&os.ModeSymlink == os.ModeSymlink {
64
-				dest, err := os.Readlink(prev)
65
-				if err != nil {
66
-					return "", err
67
-				}
68 63
 
69
-				if path.IsAbs(dest) {
70
-					prev = filepath.Join(root, dest)
71
-				} else {
72
-					prev, _ = filepath.Abs(prev)
64
+			// let's break if we're not dealing with a symlink
65
+			if stat.Mode()&os.ModeSymlink != os.ModeSymlink {
66
+				break
67
+			}
73 68
 
74
-					if prev = filepath.Join(filepath.Dir(prev), dest); len(prev) < len(root) {
75
-						prev = filepath.Join(root, filepath.Base(dest))
76
-					}
77
-				}
69
+			// process the symlink
70
+			dest, err := os.Readlink(prev)
71
+			if err != nil {
72
+				return "", err
73
+			}
74
+
75
+			if path.IsAbs(dest) {
76
+				prev = filepath.Join(root, dest)
78 77
 			} else {
79
-				break
78
+				prev, _ = filepath.Abs(prev)
79
+
80
+				dir := filepath.Dir(prev)
81
+				prev = filepath.Join(dir, dest)
82
+				if dir == root && !strings.HasPrefix(prev, root) {
83
+					prev = root
84
+				}
85
+				if len(prev) < len(root) || (len(prev) == len(root) && prev != root) {
86
+					prev = filepath.Join(root, filepath.Base(dest))
87
+				}
80 88
 			}
81 89
 		}
82 90
 	}
91
+	if prev == "/" {
92
+		prev = root
93
+	}
83 94
 	return prev, nil
84 95
 }
... ...
@@ -98,25 +98,151 @@ func TestFollowSymLinkRelativeLink(t *testing.T) {
98 98
 }
99 99
 
100 100
 func TestFollowSymLinkRelativeLinkScope(t *testing.T) {
101
-	link := "testdata/fs/a/f"
102
-
103
-	rewrite, err := FollowSymlinkInScope(link, "testdata")
104
-	if err != nil {
105
-		t.Fatal(err)
106
-	}
107
-
108
-	if expected := abs(t, "testdata/test"); expected != rewrite {
109
-		t.Fatalf("Expected %s got %s", expected, rewrite)
110
-	}
111
-
112
-	link = "testdata/fs/b/h"
113
-
114
-	rewrite, err = FollowSymlinkInScope(link, "testdata")
115
-	if err != nil {
116
-		t.Fatal(err)
117
-	}
118
-
119
-	if expected := abs(t, "testdata/root"); expected != rewrite {
120
-		t.Fatalf("Expected %s got %s", expected, rewrite)
101
+	// avoid letting symlink f lead us out of the "testdata" scope
102
+	// we don't normalize because symlink f is in scope and there is no
103
+	// information leak
104
+	{
105
+		link := "testdata/fs/a/f"
106
+
107
+		rewrite, err := FollowSymlinkInScope(link, "testdata")
108
+		if err != nil {
109
+			t.Fatal(err)
110
+		}
111
+
112
+		if expected := abs(t, "testdata/test"); expected != rewrite {
113
+			t.Fatalf("Expected %s got %s", expected, rewrite)
114
+		}
115
+	}
116
+
117
+	// avoid letting symlink f lead us out of the "testdata/fs" scope
118
+	// we don't normalize because symlink f is in scope and there is no
119
+	// information leak
120
+	{
121
+		link := "testdata/fs/a/f"
122
+
123
+		rewrite, err := FollowSymlinkInScope(link, "testdata/fs")
124
+		if err != nil {
125
+			t.Fatal(err)
126
+		}
127
+
128
+		if expected := abs(t, "testdata/fs/test"); expected != rewrite {
129
+			t.Fatalf("Expected %s got %s", expected, rewrite)
130
+		}
131
+	}
132
+
133
+	// avoid letting symlink g (pointed at by symlink h) take out of scope
134
+	// TODO: we should probably normalize to scope here because ../[....]/root
135
+	// is out of scope and we leak information
136
+	{
137
+		link := "testdata/fs/b/h"
138
+
139
+		rewrite, err := FollowSymlinkInScope(link, "testdata")
140
+		if err != nil {
141
+			t.Fatal(err)
142
+		}
143
+
144
+		if expected := abs(t, "testdata/root"); expected != rewrite {
145
+			t.Fatalf("Expected %s got %s", expected, rewrite)
146
+		}
147
+	}
148
+
149
+	// avoid letting allowing symlink e lead us to ../b
150
+	// normalize to the "testdata/fs/a"
151
+	{
152
+		link := "testdata/fs/a/e"
153
+
154
+		rewrite, err := FollowSymlinkInScope(link, "testdata/fs/a")
155
+		if err != nil {
156
+			t.Fatal(err)
157
+		}
158
+
159
+		if expected := abs(t, "testdata/fs/a"); expected != rewrite {
160
+			t.Fatalf("Expected %s got %s", expected, rewrite)
161
+		}
162
+	}
163
+
164
+	// avoid letting symlink -> ../directory/file escape from scope
165
+	// normalize to "testdata/fs/j"
166
+	{
167
+		link := "testdata/fs/j/k"
168
+
169
+		rewrite, err := FollowSymlinkInScope(link, "testdata/fs/j")
170
+		if err != nil {
171
+			t.Fatal(err)
172
+		}
173
+
174
+		if expected := abs(t, "testdata/fs/j"); expected != rewrite {
175
+			t.Fatalf("Expected %s got %s", expected, rewrite)
176
+		}
177
+	}
178
+
179
+	// make sure we don't allow escaping to /
180
+	// normalize to dir
181
+	{
182
+		dir, err := ioutil.TempDir("", "docker-fs-test")
183
+		if err != nil {
184
+			t.Fatal(err)
185
+		}
186
+		defer os.RemoveAll(dir)
187
+
188
+		linkFile := filepath.Join(dir, "foo")
189
+		os.Mkdir(filepath.Join(dir, ""), 0700)
190
+		os.Symlink("/", linkFile)
191
+
192
+		rewrite, err := FollowSymlinkInScope(linkFile, dir)
193
+		if err != nil {
194
+			t.Fatal(err)
195
+		}
196
+
197
+		if rewrite != dir {
198
+			t.Fatalf("Expected %s got %s", dir, rewrite)
199
+		}
200
+	}
201
+
202
+	// make sure we don't allow escaping to /
203
+	// normalize to dir
204
+	{
205
+		dir, err := ioutil.TempDir("", "docker-fs-test")
206
+		if err != nil {
207
+			t.Fatal(err)
208
+		}
209
+		defer os.RemoveAll(dir)
210
+
211
+		linkFile := filepath.Join(dir, "foo")
212
+		os.Mkdir(filepath.Join(dir, ""), 0700)
213
+		os.Symlink("/../../", linkFile)
214
+
215
+		rewrite, err := FollowSymlinkInScope(linkFile, dir)
216
+		if err != nil {
217
+			t.Fatal(err)
218
+		}
219
+
220
+		if rewrite != dir {
221
+			t.Fatalf("Expected %s got %s", dir, rewrite)
222
+		}
223
+	}
224
+
225
+	// make sure we stay in scope without leaking information
226
+	// this also checks for escaping to /
227
+	// normalize to dir
228
+	{
229
+		dir, err := ioutil.TempDir("", "docker-fs-test")
230
+		if err != nil {
231
+			t.Fatal(err)
232
+		}
233
+		defer os.RemoveAll(dir)
234
+
235
+		linkFile := filepath.Join(dir, "foo")
236
+		os.Mkdir(filepath.Join(dir, ""), 0700)
237
+		os.Symlink("../../", linkFile)
238
+
239
+		rewrite, err := FollowSymlinkInScope(linkFile, dir)
240
+		if err != nil {
241
+			t.Fatal(err)
242
+		}
243
+
244
+		if rewrite != dir {
245
+			t.Fatalf("Expected %s got %s", dir, rewrite)
246
+		}
121 247
 	}
122 248
 }
123 249
new file mode 120000
... ...
@@ -0,0 +1 @@
0
+../i/a
0 1
\ No newline at end of file