ファイル名の上限を拡大し、動画の配信・削除を os.Root に置き換え(エピソード ID は 64 バイト上限)
This commit is contained in:
@@ -135,7 +135,23 @@ func TestVideoPathTraversalAndSymlinkAreRejected(t *testing.T) {
|
||||
if err := os.Symlink(outside, filepath.Join(dir, "linked-directory")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, relative := range []string{"../outside.mp4", "show/../../outside.mp4", outsideFile, "link.mp4", "linked-directory/outside.mp4", "linked-directory/missing.mp4"} {
|
||||
relativeOutsideFile, err := filepath.Rel(dir, outsideFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
relativeOutsideDir, err := filepath.Rel(dir, outside)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(relativeOutsideFile, filepath.Join(dir, "relative-link.mp4")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(relativeOutsideDir, filepath.Join(dir, "relative-linked-directory")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, relative := range []string{"../outside.mp4", "show/../../outside.mp4", outsideFile,
|
||||
"link.mp4", "linked-directory/outside.mp4", "linked-directory/missing.mp4",
|
||||
"relative-link.mp4", "relative-linked-directory/outside.mp4", "relative-linked-directory/missing.mp4"} {
|
||||
jobs := &fakeJobStore{jobs: map[string]store.Job{"ep1": libraryJob(1, relative)}}
|
||||
handler := newTestHandlerWithVideoDir(&fakeFetcher{}, jobs, dir, nil)
|
||||
if got := getPage(handler, "/videos/1").Code; got != http.StatusNotFound {
|
||||
@@ -146,6 +162,89 @@ func TestVideoPathTraversalAndSymlinkAreRejected(t *testing.T) {
|
||||
t.Errorf("delete path %q status=%d jobs=%d", relative, deleted.Code, len(jobs.jobs))
|
||||
}
|
||||
}
|
||||
if got, err := os.ReadFile(outsideFile); err != nil || string(got) != "secret" {
|
||||
t.Fatalf("outside target was changed: content=%q err=%v", got, err)
|
||||
}
|
||||
for _, link := range []string{"link.mp4", "linked-directory", "relative-link.mp4", "relative-linked-directory"} {
|
||||
if _, err := os.Lstat(filepath.Join(dir, link)); err != nil {
|
||||
t.Errorf("rejected link %q was changed: %v", link, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVideoSymlinkWithinRootCanBeServedAndDeleted(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
target := filepath.Join(dir, "target.mp4")
|
||||
if err := os.WriteFile(target, []byte("video"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink("target.mp4", filepath.Join(dir, "link.mp4")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
jobs := &fakeJobStore{jobs: map[string]store.Job{"ep1": libraryJob(1, "link.mp4")}}
|
||||
handler := newTestHandlerWithVideoDir(&fakeFetcher{}, jobs, dir, nil)
|
||||
if response := getPage(handler, "/videos/1"); response.Code != http.StatusOK || response.Body.String() != "video" {
|
||||
t.Fatalf("serve: status=%d body=%q", response.Code, response.Body.String())
|
||||
}
|
||||
if response := postForm(handler, "/library/1/delete", nil); response.Code != http.StatusSeeOther || len(jobs.jobs) != 0 {
|
||||
t.Fatalf("delete: status=%d jobs=%d", response.Code, len(jobs.jobs))
|
||||
}
|
||||
if _, err := os.Stat(target); err != nil {
|
||||
t.Fatalf("symlink target was removed: %v", err)
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(dir, "link.mp4")); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("symlink itself remains: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVideoSymlinkedDirectoryWithinRootCanBeServedAndDeleted(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.Mkdir(filepath.Join(dir, "target"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target := filepath.Join(dir, "target", "video.mp4")
|
||||
if err := os.WriteFile(target, []byte("video"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink("target", filepath.Join(dir, "linked-directory")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
jobs := &fakeJobStore{jobs: map[string]store.Job{"ep1": libraryJob(1, "linked-directory/video.mp4")}}
|
||||
handler := newTestHandlerWithVideoDir(&fakeFetcher{}, jobs, dir, nil)
|
||||
if response := getPage(handler, "/videos/1"); response.Code != http.StatusOK || response.Body.String() != "video" {
|
||||
t.Fatalf("serve: status=%d body=%q", response.Code, response.Body.String())
|
||||
}
|
||||
if response := postForm(handler, "/library/1/delete", nil); response.Code != http.StatusSeeOther || len(jobs.jobs) != 0 {
|
||||
t.Fatalf("delete: status=%d jobs=%d", response.Code, len(jobs.jobs))
|
||||
}
|
||||
if _, err := os.Stat(target); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("target file remains: %v", err)
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(dir, "linked-directory")); err != nil {
|
||||
t.Fatalf("directory symlink was removed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVideoAbsoluteSymlinkWithinRootIsRejectedByOSRoot(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
target := filepath.Join(dir, "target.mp4")
|
||||
if err := os.WriteFile(target, []byte("video"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(target, filepath.Join(dir, "absolute-link.mp4")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
jobs := &fakeJobStore{jobs: map[string]store.Job{"ep1": libraryJob(1, "absolute-link.mp4")}}
|
||||
handler := newTestHandlerWithVideoDir(&fakeFetcher{}, jobs, dir, nil)
|
||||
if got := getPage(handler, "/videos/1").Code; got != http.StatusNotFound {
|
||||
t.Fatalf("absolute symlink serve status=%d", got)
|
||||
}
|
||||
if response := postForm(handler, "/library/1/delete", nil); response.Code != http.StatusConflict || len(jobs.jobs) != 1 {
|
||||
t.Fatalf("absolute symlink delete: status=%d jobs=%d", response.Code, len(jobs.jobs))
|
||||
}
|
||||
if _, err := os.Stat(target); err != nil {
|
||||
t.Fatalf("target was changed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteVideoFileThenRecord(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user