Skip to content

Commit 459bcfc

Browse files
Merge pull request #1228 from openziti/v2.0.2_drive_symlinks
fix for drive backend symlink traversal
2 parents 164acf3 + 16061d2 commit 459bcfc

4 files changed

Lines changed: 355 additions & 17 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
.vscode
55
.contexts
66
CLAUDE.md
7-
/.claude/
7+
.claude
8+
.codex
89
AGENTS.md
910
*.db
1011
/automated-release-build/

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## v2.0.2
4+
5+
FIX: The `drive` backend mode WebDAV implementation now prevents symlink traversal outside the configured shared directory. `Stat`, `OpenFile`, `Mkdir`, `RemoveAll`, and `Rename` now reject symlinks that resolve outside the drive root while continuing to allow symlinks that resolve within that tree. This fixes GHSA-74m3-9qvm-rp9h.
6+
37
## v2.0.1
48

59
FEATURE: Added several new `admin` API endpoints for interfacing with additional management controls: finding limit classes by label, finding applied/applying/removing limit classes from accounts, getting/setting skip interstitial status for an account (https://github.com/openziti/zrok/issues/1210)

drives/davServer/file.go

Lines changed: 179 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,28 @@ type File interface {
5757
io.Writer
5858
}
5959

60+
type namedFileInfo struct {
61+
os.FileInfo
62+
name string
63+
}
64+
65+
func (fi namedFileInfo) Name() string {
66+
return fi.name
67+
}
68+
69+
type osDirFile struct {
70+
*os.File
71+
statName string
72+
}
73+
74+
func (f *osDirFile) Stat() (os.FileInfo, error) {
75+
info, err := f.File.Stat()
76+
if err != nil {
77+
return nil, err
78+
}
79+
return namedFileInfo{FileInfo: info, name: f.statName}, nil
80+
}
81+
6082
type webdavFile struct {
6183
File
6284
name string
@@ -130,54 +152,195 @@ func (d Dir) resolve(name string) string {
130152
return filepath.Join(dir, filepath.FromSlash(slashClean(name)))
131153
}
132154

155+
func (d Dir) rootPath() (string, error) {
156+
dir := string(d)
157+
if dir == "" {
158+
dir = "."
159+
}
160+
root, err := filepath.Abs(dir)
161+
if err != nil {
162+
return "", err
163+
}
164+
return filepath.EvalSymlinks(root)
165+
}
166+
167+
func splitVirtualPath(name string) []string {
168+
switch name {
169+
case "", ".", "/":
170+
return nil
171+
}
172+
name = strings.TrimPrefix(name, "/")
173+
if name == "" {
174+
return nil
175+
}
176+
return strings.Split(name, "/")
177+
}
178+
179+
func joinVirtualPath(base string, segments []string) string {
180+
for _, segment := range segments {
181+
base = filepath.Join(base, filepath.FromSlash(segment))
182+
}
183+
return base
184+
}
185+
186+
func isWithinRoot(root, target string) bool {
187+
rel, err := filepath.Rel(root, target)
188+
if err != nil {
189+
return false
190+
}
191+
return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)))
192+
}
193+
194+
func virtualName(name string) string {
195+
return path.Base(slashClean(name))
196+
}
197+
198+
func (d Dir) resolveBoundedPath(name string, followLeaf, allowMissingLeaf, allowMissingTail bool) (string, error) {
199+
if d.resolve(name) == "" {
200+
return "", os.ErrNotExist
201+
}
202+
root, err := d.rootPath()
203+
if err != nil {
204+
return "", err
205+
}
206+
207+
current := root
208+
segments := splitVirtualPath(slashClean(name))
209+
symlinks := 0
210+
211+
for len(segments) > 0 {
212+
segment := segments[0]
213+
segments = segments[1:]
214+
215+
next := filepath.Join(current, filepath.FromSlash(segment))
216+
final := len(segments) == 0
217+
218+
info, err := os.Lstat(next)
219+
if err != nil {
220+
if os.IsNotExist(err) && (allowMissingTail || (allowMissingLeaf && final)) {
221+
return joinVirtualPath(current, append([]string{segment}, segments...)), nil
222+
}
223+
return "", err
224+
}
225+
226+
if info.Mode()&os.ModeSymlink == 0 {
227+
current = next
228+
continue
229+
}
230+
231+
if !followLeaf && final {
232+
return next, nil
233+
}
234+
235+
symlinks++
236+
if symlinks > 255 {
237+
return "", os.ErrInvalid
238+
}
239+
240+
target, err := os.Readlink(next)
241+
if err != nil {
242+
return "", err
243+
}
244+
if !filepath.IsAbs(target) {
245+
target = filepath.Join(current, target)
246+
}
247+
target = filepath.Clean(target)
248+
249+
if !isWithinRoot(root, target) {
250+
return "", os.ErrPermission
251+
}
252+
253+
relTarget, err := filepath.Rel(root, target)
254+
if err != nil {
255+
return "", os.ErrPermission
256+
}
257+
targetSegments := splitVirtualPath(filepath.ToSlash(relTarget))
258+
259+
current = root
260+
segments = append(targetSegments, segments...)
261+
}
262+
263+
return current, nil
264+
}
265+
133266
func (d Dir) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
134-
if name = d.resolve(name); name == "" {
135-
return os.ErrNotExist
267+
name, err := d.resolveBoundedPath(name, false, true, false)
268+
if err != nil {
269+
return err
136270
}
137271
return os.Mkdir(name, perm)
138272
}
139273

140274
func (d Dir) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (File, error) {
141-
if name = d.resolve(name); name == "" {
142-
return nil, os.ErrNotExist
275+
resolvedName, err := d.resolveBoundedPath(name, true, flag&os.O_CREATE != 0, false)
276+
if err != nil {
277+
return nil, err
143278
}
144-
f, err := os.OpenFile(name, flag, perm)
279+
f, err := os.OpenFile(resolvedName, flag, perm)
145280
if err != nil {
146281
return nil, err
147282
}
148-
return &webdavFile{f, name}, nil
283+
return &webdavFile{
284+
File: &osDirFile{
285+
File: f,
286+
statName: virtualName(name),
287+
},
288+
name: resolvedName,
289+
}, nil
149290
}
150291

151292
func (d Dir) RemoveAll(ctx context.Context, name string) error {
152-
if name = d.resolve(name); name == "" {
293+
resolvedName := d.resolve(name)
294+
if resolvedName == "" {
153295
return os.ErrNotExist
154296
}
155-
if name == filepath.Clean(string(d)) {
297+
if resolvedName == filepath.Clean(string(d)) {
156298
// Prohibit removing the virtual root directory.
157299
return os.ErrInvalid
158300
}
159-
return os.RemoveAll(name)
301+
var err error
302+
resolvedName, err = d.resolveBoundedPath(name, false, false, true)
303+
if err != nil {
304+
return err
305+
}
306+
return os.RemoveAll(resolvedName)
160307
}
161308

162309
func (d Dir) Rename(ctx context.Context, oldName, newName string) error {
163-
if oldName = d.resolve(oldName); oldName == "" {
310+
resolvedOldName := d.resolve(oldName)
311+
if resolvedOldName == "" {
164312
return os.ErrNotExist
165313
}
166-
if newName = d.resolve(newName); newName == "" {
314+
resolvedNewName := d.resolve(newName)
315+
if resolvedNewName == "" {
167316
return os.ErrNotExist
168317
}
169-
if root := filepath.Clean(string(d)); root == oldName || root == newName {
318+
if root := filepath.Clean(string(d)); root == resolvedOldName || root == resolvedNewName {
170319
// Prohibit renaming from or to the virtual root directory.
171320
return os.ErrInvalid
172321
}
173-
return os.Rename(oldName, newName)
322+
var err error
323+
resolvedOldName, err = d.resolveBoundedPath(oldName, false, false, false)
324+
if err != nil {
325+
return err
326+
}
327+
resolvedNewName, err = d.resolveBoundedPath(newName, false, true, false)
328+
if err != nil {
329+
return err
330+
}
331+
return os.Rename(resolvedOldName, resolvedNewName)
174332
}
175333

176334
func (d Dir) Stat(ctx context.Context, name string) (os.FileInfo, error) {
177-
if name = d.resolve(name); name == "" {
178-
return nil, os.ErrNotExist
335+
resolvedName, err := d.resolveBoundedPath(name, true, false, false)
336+
if err != nil {
337+
return nil, err
338+
}
339+
info, err := os.Stat(resolvedName)
340+
if err != nil {
341+
return nil, err
179342
}
180-
return os.Stat(name)
343+
return namedFileInfo{FileInfo: info, name: virtualName(name)}, nil
181344
}
182345

183346
// NewMemFS returns a new in-memory FileSystem implementation.

0 commit comments

Comments
 (0)