package admin import ( "net/http/httptest" "os" "path/filepath" "strings" "testing" ) // TestFilesStayInsideDataDir: the raw file editor must reject request paths // that escape the data directory via .. segments — writes, reads, and // deletes alike. func TestFilesStayInsideDataDir(t *testing.T) { root := t.TempDir() dir := filepath.Join(root, "data") if err := os.Mkdir(dir, 0700); err != nil { t.Fatal(err) } target := filepath.Join(root, "outside.txt") if err := os.WriteFile(target, []byte("original"), 0600); err != nil { t.Fatal(err) } s := &AdminServer{dataDir: dir, undoStack: NewUndoStack(dir)} // Write traversal. r := httptest.NewRequest("PUT", "/api/files?path=../outside.txt", strings.NewReader("overwritten")) w := httptest.NewRecorder() s.handleFiles(w, r) got, err := os.ReadFile(target) if err != nil { t.Fatal(err) } if string(got) != "original" { t.Fatalf("PUT traversal overwrote outside the data dir: content=%q response=%s", got, w.Body.String()) } // Read traversal must not reveal the file either. r = httptest.NewRequest("GET", "/api/files?path=../outside.txt", nil) w = httptest.NewRecorder() s.handleFiles(w, r) if strings.Contains(w.Body.String(), "original") { t.Fatalf("GET traversal read outside the data dir: %s", w.Body.String()) } // Deep traversal segments are rejected too. r = httptest.NewRequest("DELETE", "/api/files?path=rooms/../../outside.txt", nil) w = httptest.NewRecorder() s.handleFiles(w, r) if got, err := os.ReadFile(target); err != nil || string(got) != "original" { t.Fatalf("DELETE traversal removed the outside file: %q %v", got, err) } // Renaming into a traversal path is rejected as well. if err := os.WriteFile(filepath.Join(dir, "inside.txt"), []byte("x"), 0600); err != nil { t.Fatal(err) } r = httptest.NewRequest("POST", "/api/files/rename", strings.NewReader(`{"path":"inside.txt","new_name":"../escaped.txt"}`)) // The rename handler validates the new name itself; ../ in a name is // rejected by the slash check, so this only guards the source path. w = httptest.NewRecorder() s.handleFileRename(w, r) if _, err := os.Stat(filepath.Join(root, "escaped.txt")); err == nil { t.Fatal("rename escaped the data directory") } }