This commit is contained in:
@@ -5,6 +5,7 @@ package store
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -364,8 +365,8 @@ func TestStoreUploadAndListMedia(t *testing.T) {
|
||||
if _, err := s.MediaPath(name); err != nil {
|
||||
t.Fatalf("MediaPath cannot find the upload: %v", err)
|
||||
}
|
||||
if _, err := s.MediaPath("../" + name); err != nil {
|
||||
t.Fatalf("a base name should still resolve: %v", err)
|
||||
if _, err := s.MediaPath("../" + name); err == nil {
|
||||
t.Fatal("MediaPath accepted a name that climbs out of the media directory")
|
||||
}
|
||||
if _, err := s.MediaPath("notes.txt"); err == nil {
|
||||
t.Fatal("MediaPath accepted a name that is not an image")
|
||||
@@ -383,6 +384,75 @@ func TestStoreUploadAndListMedia(t *testing.T) {
|
||||
if s.DeleteMedia("/etc/passwd") {
|
||||
t.Fatal("DeleteMedia accepted non-media URL")
|
||||
}
|
||||
// A URL that resolves to a directory removes nothing: the media and
|
||||
// avatar directories are namespaces, not deletable content.
|
||||
if s.DeleteMedia("/media/") {
|
||||
t.Fatal("DeleteMedia accepted the media directory itself")
|
||||
}
|
||||
}
|
||||
|
||||
// A profile photo is account state, not library content: it lives under
|
||||
// avatars/ inside the media directory, which the listing skips, while
|
||||
// the public route serves it and the delete path removes it.
|
||||
func TestStoreAvatarLivesOutsideTheLibrary(t *testing.T) {
|
||||
s, _ := newStore(t)
|
||||
webpData := append([]byte("RIFF"), 0, 0, 0, 0)
|
||||
webpData = append(webpData, []byte("WEBPVP8 ")...)
|
||||
url, err := s.StoreAvatar(webpData)
|
||||
if err != nil {
|
||||
t.Fatalf("StoreAvatar: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(url, "/media/avatars/") || !strings.HasSuffix(url, ".webp") {
|
||||
t.Fatalf("url = %q", url)
|
||||
}
|
||||
name := strings.TrimPrefix(url, "/media/") // avatars/<uuid>.webp
|
||||
if got, err := s.MediaPath(name); err != nil {
|
||||
t.Fatalf("MediaPath cannot find the avatar: %v", err)
|
||||
} else if !strings.Contains(filepath.ToSlash(got), "/media/avatars/") {
|
||||
t.Fatalf("avatar path = %q", got)
|
||||
}
|
||||
if _, err := s.MediaPath("avatars/" + path.Base(name)); err != nil {
|
||||
t.Fatalf("MediaPath cannot find the avatar by its route shape: %v", err)
|
||||
}
|
||||
if media := s.ListMedia(); len(media) != 0 {
|
||||
t.Fatalf("the avatar leaked into the library: %v", media)
|
||||
}
|
||||
if !s.DeleteMedia(url) {
|
||||
t.Fatal("DeleteMedia failed for the avatar")
|
||||
}
|
||||
if s.DeleteMedia(url) {
|
||||
t.Fatal("DeleteMedia succeeded twice")
|
||||
}
|
||||
if _, err := s.MediaPath(name); err == nil {
|
||||
t.Fatal("the avatar survived its deletion")
|
||||
}
|
||||
}
|
||||
|
||||
// The avatar namespace is exactly one level below the media directory:
|
||||
// anything deeper, anything outside it and anything without an image
|
||||
// extension is refused, so the public route gains no reach.
|
||||
func TestMediaPathRefusesNonMediaNames(t *testing.T) {
|
||||
s, _ := newStore(t)
|
||||
for _, name := range []string{
|
||||
"avatars/../x.webp",
|
||||
"avatars/a/b.webp",
|
||||
"sub/x.webp",
|
||||
"avatarss/x.webp",
|
||||
"avatars/x.txt",
|
||||
"avatars/",
|
||||
"avatars",
|
||||
"..",
|
||||
"a/../b.webp",
|
||||
} {
|
||||
if got, err := s.MediaPath(name); err == nil {
|
||||
t.Fatalf("MediaPath(%q) = %q, want a refusal", name, got)
|
||||
}
|
||||
}
|
||||
// The avatar directory itself is not deletable content, with or
|
||||
// without a trailing slash.
|
||||
if s.DeleteMedia("/media/avatars") || s.DeleteMedia("/media/avatars/") {
|
||||
t.Fatal("DeleteMedia removed the avatar directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreUploadSVGAndDimensions(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user