aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/api_files_path_test.go
blob: 6d660fbf2648683a2cf5bf10521a9ca64cd1154a (plain)
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")
	}
}