1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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")
}
}
|