diff options
| author | historia <[not public]> | 2026-09-15 20:35:23 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-15 20:35:23 -0400 |
| commit | fcb806c3445ce56f2e57e208de773b4dac957c8d (patch) | |
| tree | 3b08c3d51bfe5456dcd215449cf07509aca2353a /internal/admin/api_files_path_test.go | |
| parent | 69235cac5b3da5ff0dc20310e7474cabd8e63ba3 (diff) | |
| download | thehouseoficarus-fcb806c3445ce56f2e57e208de773b4dac957c8d.tar.gz | |
fix: owner re-entrancy deadlock, telnet write queue, recovery/refund money safety, admin path traversal
Diffstat (limited to 'internal/admin/api_files_path_test.go')
| -rw-r--r-- | internal/admin/api_files_path_test.go | 66 |
1 files changed, 66 insertions, 0 deletions
diff --git a/internal/admin/api_files_path_test.go b/internal/admin/api_files_path_test.go new file mode 100644 index 0000000..6d660fb --- /dev/null +++ b/internal/admin/api_files_path_test.go @@ -0,0 +1,66 @@ +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") + } +} |
