This commit is contained in:
曜日 2026-07-09 21:25:34 +08:00 committed by GitHub
commit b11796dd1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 40 additions and 0 deletions

View file

@ -159,6 +159,38 @@ func TestHomepageFileAndDir(t *testing.T) {
} }
} }
func TestSymlinkEscapeRefused(t *testing.T) {
srv, dataDir := newTestServer(t)
// Create a symlink inside alice's public_html that points to secret.txt
// outside the member area. The selector should be refused.
link := filepath.Join(dataDir, "users", "alice", "public_html", "leak.txt")
if err := os.Symlink(filepath.Join(dataDir, "secret.txt"), link); err != nil {
t.Fatal(err)
}
r := srv.Resolve("/~alice/leak.txt", false, "")
if r.Kind == KindText && strings.Contains(r.Text, "TOP SECRET") {
t.Fatal("symlink escape: resolved path pointed outside member area but content was served")
}
// Symlink to a subdirectory should still work.
subdir := filepath.Join(dataDir, "users", "alice", "public_html", "sub")
if err := os.Mkdir(subdir, 0o755); err != nil {
t.Fatal(err)
}
inner := filepath.Join(subdir, "hello.txt")
if err := os.WriteFile(inner, []byte("inside"), 0o644); err != nil {
t.Fatal(err)
}
// Symlink inside the area pointing to another path within the area is fine.
sym := filepath.Join(dataDir, "users", "alice", "public_html", "ok-link")
if err := os.Symlink(inner, sym); err != nil {
t.Fatal(err)
}
r2 := srv.Resolve("/~alice/ok-link", false, "")
if r2.Kind != KindText || !strings.Contains(r2.Text, "inside") {
t.Fatalf("symlink within area should be allowed: got %+v", r2)
}
}
func TestPathTraversalRefused(t *testing.T) { func TestPathTraversalRefused(t *testing.T) {
srv, _ := newTestServer(t) srv, _ := newTestServer(t)
for _, sel := range []string{ for _, sel := range []string{

View file

@ -310,6 +310,14 @@ func (s *Server) userTree(prefix, sel, area string) Response {
if target != base && !strings.HasPrefix(target, base+string(os.PathSeparator)) { if target != base && !strings.HasPrefix(target, base+string(os.PathSeparator)) {
return errResp("forbidden") return errResp("forbidden")
} }
// Symlink escape guard: resolve symlinks and re-check the real path stays
// within the member's area. A symlink created inside public_html that
// points outside would pass the lexical check but leak external content.
if resolved, err := filepath.EvalSymlinks(target); err == nil {
if resolved != base && !strings.HasPrefix(resolved, base+string(os.PathSeparator)) {
return errResp("forbidden")
}
}
info, err := os.Stat(target) info, err := os.Stat(target)
if err != nil { if err != nil {