Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions internal/gopher/gopher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,23 @@ func TestPathTraversalRefused(t *testing.T) {
}
}

func TestSymlinkEscapeRefused(t *testing.T) {
srv, dataDir := newTestServer(t)
home := filepath.Join(dataDir, "users", "alice", "public_html")
link := filepath.Join(home, "leak.txt")
if err := os.Symlink(filepath.Join(dataDir, "secret.txt"), link); err != nil {
t.Skipf("symlink unavailable: %v", err)
}

r := srv.Resolve("/~alice/leak.txt", false, "")
if r.Kind != KindError {
t.Fatalf("symlink escape should be refused, got %v", r.Kind)
}
if strings.Contains(r.Text, "TOP SECRET") || strings.Contains(string(r.Data), "TOP SECRET") {
t.Fatalf("symlink escape leaked the secret file: %+v", r)
}
}

func TestNewsPublicVsAuthed(t *testing.T) {
srv, _ := newTestServer(t)

Expand Down
19 changes: 15 additions & 4 deletions internal/gopher/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,8 @@ func (s *Server) userTree(prefix, sel, area string) Response {
base := filepath.Join(s.dataDir, "users", name, area)

// Confine sub to base. Cleaning as an absolute path drops any leading ".."
// components; the HasPrefix re-check is belt-and-suspenders against symlink
// or edge cases.
// components; the HasPrefix re-check is belt-and-suspenders against edge
// cases before resolving symlinks below.
clean := filepath.Clean("/" + sub)
target := filepath.Join(base, clean)
if target != base && !strings.HasPrefix(target, base+string(os.PathSeparator)) {
Expand All @@ -315,16 +315,27 @@ func (s *Server) userTree(prefix, sel, area string) Response {
if err != nil {
return errResp("not found")
}
baseReal, err := filepath.EvalSymlinks(base)
if err != nil {
return errResp("not found")
}
targetReal, err := filepath.EvalSymlinks(target)
if err != nil {
return errResp("not found")
}
if targetReal != baseReal && !strings.HasPrefix(targetReal, baseReal+string(os.PathSeparator)) {
return errResp("forbidden")
}
linkBase := "/~" + name
if prefix == "files" {
linkBase = "/files/~" + name
}
rel := strings.TrimPrefix(strings.TrimPrefix(target, base), string(os.PathSeparator))

if info.IsDir() {
return s.dirMenu(target, linkBase, rel, name)
return s.dirMenu(targetReal, linkBase, rel, name)
}
data, err := os.ReadFile(target)
data, err := os.ReadFile(targetReal)
if err != nil {
return errResp("unreadable")
}
Expand Down
Loading