mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 14:27:27 +00:00
fix(gopher): block symlink escapes (#85)
Co-authored-by: rissrice2105-agent <rissrice2105-agent@users.noreply.github.com>
This commit is contained in:
parent
e38c0935f4
commit
c2a06f1cdf
2 changed files with 32 additions and 4 deletions
|
|
@ -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) {
|
func TestNewsPublicVsAuthed(t *testing.T) {
|
||||||
srv, _ := newTestServer(t)
|
srv, _ := newTestServer(t)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -303,8 +303,8 @@ func (s *Server) userTree(prefix, sel, area string) Response {
|
||||||
base := filepath.Join(s.dataDir, "users", name, area)
|
base := filepath.Join(s.dataDir, "users", name, area)
|
||||||
|
|
||||||
// Confine sub to base. Cleaning as an absolute path drops any leading ".."
|
// Confine sub to base. Cleaning as an absolute path drops any leading ".."
|
||||||
// components; the HasPrefix re-check is belt-and-suspenders against symlink
|
// components; the HasPrefix re-check is belt-and-suspenders against edge
|
||||||
// or edge cases.
|
// cases before resolving symlinks below.
|
||||||
clean := filepath.Clean("/" + sub)
|
clean := filepath.Clean("/" + sub)
|
||||||
target := filepath.Join(base, clean)
|
target := filepath.Join(base, clean)
|
||||||
if target != base && !strings.HasPrefix(target, base+string(os.PathSeparator)) {
|
if target != base && !strings.HasPrefix(target, base+string(os.PathSeparator)) {
|
||||||
|
|
@ -315,6 +315,17 @@ func (s *Server) userTree(prefix, sel, area string) Response {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errResp("not found")
|
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
|
linkBase := "/~" + name
|
||||||
if prefix == "files" {
|
if prefix == "files" {
|
||||||
linkBase = "/files/~" + name
|
linkBase = "/files/~" + name
|
||||||
|
|
@ -322,9 +333,9 @@ func (s *Server) userTree(prefix, sel, area string) Response {
|
||||||
rel := strings.TrimPrefix(strings.TrimPrefix(target, base), string(os.PathSeparator))
|
rel := strings.TrimPrefix(strings.TrimPrefix(target, base), string(os.PathSeparator))
|
||||||
|
|
||||||
if info.IsDir() {
|
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 {
|
if err != nil {
|
||||||
return errResp("unreadable")
|
return errResp("unreadable")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue