Clean paths when looking in Storage (#19124)
* Clean paths when looking in Storage Ensure paths are clean for minio aswell as local storage. Use url.Path not RequestURI/EscapedPath in storageHandler. Signed-off-by: Andrew Thornton <art27@cantab.net> * Apply suggestions from code review Co-authored-by: Lauris BH <lauris@nix.lv>
This commit is contained in:
parent
d2c165811a
commit
3f71ab9a12
4 changed files with 40 additions and 51 deletions
|
@ -6,7 +6,6 @@ package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
@ -18,8 +17,6 @@ import (
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrLocalPathNotSupported represents an error that path is not supported
|
|
||||||
var ErrLocalPathNotSupported = errors.New("local path is not supported")
|
|
||||||
var _ ObjectStorage = &LocalStorage{}
|
var _ ObjectStorage = &LocalStorage{}
|
||||||
|
|
||||||
// LocalStorageType is the type descriptor for local storage
|
// LocalStorageType is the type descriptor for local storage
|
||||||
|
@ -62,21 +59,18 @@ func NewLocalStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *LocalStorage) buildLocalPath(p string) string {
|
||||||
|
return filepath.Join(l.dir, path.Clean("/" + strings.ReplaceAll(p, "\\", "/"))[1:])
|
||||||
|
}
|
||||||
|
|
||||||
// Open a file
|
// Open a file
|
||||||
func (l *LocalStorage) Open(path string) (Object, error) {
|
func (l *LocalStorage) Open(path string) (Object, error) {
|
||||||
if !isLocalPathValid(path) {
|
return os.Open(l.buildLocalPath(path))
|
||||||
return nil, ErrLocalPathNotSupported
|
|
||||||
}
|
|
||||||
return os.Open(filepath.Join(l.dir, path))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save a file
|
// Save a file
|
||||||
func (l *LocalStorage) Save(path string, r io.Reader, size int64) (int64, error) {
|
func (l *LocalStorage) Save(path string, r io.Reader, size int64) (int64, error) {
|
||||||
if !isLocalPathValid(path) {
|
p := l.buildLocalPath(path)
|
||||||
return 0, ErrLocalPathNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
p := filepath.Join(l.dir, path)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(p), os.ModePerm); err != nil {
|
if err := os.MkdirAll(filepath.Dir(p), os.ModePerm); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
@ -116,24 +110,12 @@ func (l *LocalStorage) Save(path string, r io.Reader, size int64) (int64, error)
|
||||||
|
|
||||||
// Stat returns the info of the file
|
// Stat returns the info of the file
|
||||||
func (l *LocalStorage) Stat(path string) (os.FileInfo, error) {
|
func (l *LocalStorage) Stat(path string) (os.FileInfo, error) {
|
||||||
return os.Stat(filepath.Join(l.dir, path))
|
return os.Stat(l.buildLocalPath(path))
|
||||||
}
|
|
||||||
|
|
||||||
func isLocalPathValid(p string) bool {
|
|
||||||
a := path.Clean(p)
|
|
||||||
if strings.HasPrefix(a, "../") || strings.HasPrefix(a, "..\\") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return a == p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete delete a file
|
// Delete delete a file
|
||||||
func (l *LocalStorage) Delete(path string) error {
|
func (l *LocalStorage) Delete(path string) error {
|
||||||
if !isLocalPathValid(path) {
|
return util.Remove(l.buildLocalPath(path))
|
||||||
return ErrLocalPathNotSupported
|
|
||||||
}
|
|
||||||
p := filepath.Join(l.dir, path)
|
|
||||||
return util.Remove(p)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL gets the redirect URL to a file
|
// URL gets the redirect URL to a file
|
||||||
|
|
|
@ -10,36 +10,44 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLocalPathIsValid(t *testing.T) {
|
func TestBuildLocalPath(t *testing.T) {
|
||||||
kases := []struct {
|
kases := []struct {
|
||||||
path string
|
localDir string
|
||||||
valid bool
|
path string
|
||||||
|
expected string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
"a",
|
||||||
|
"0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
"a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
"a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"../a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
"a",
|
||||||
false,
|
"../0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
|
"a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"a\\0\\a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
"a",
|
||||||
true,
|
"0\\a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
|
"a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"b/../a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
"b",
|
||||||
false,
|
"a/../0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
|
"b/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"..\\a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
"b",
|
||||||
false,
|
"a\\..\\0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
|
"b/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, k := range kases {
|
for _, k := range kases {
|
||||||
t.Run(k.path, func(t *testing.T) {
|
t.Run(k.path, func(t *testing.T) {
|
||||||
assert.EqualValues(t, k.valid, isLocalPathValid(k.path))
|
l := LocalStorage{dir: k.localDir}
|
||||||
|
|
||||||
|
assert.EqualValues(t, k.expected, l.buildLocalPath(k.path))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,7 +117,7 @@ func NewMinioStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MinioStorage) buildMinioPath(p string) string {
|
func (m *MinioStorage) buildMinioPath(p string) string {
|
||||||
return strings.TrimPrefix(path.Join(m.basePath, p), "/")
|
return strings.TrimPrefix(path.Join(m.basePath, path.Clean("/" + strings.ReplaceAll(p, "\\", "/"))[1:]), "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open open a file
|
// Open open a file
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
@ -28,6 +27,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func storageHandler(storageSetting setting.Storage, prefix string, objStore storage.ObjectStorage) func(next http.Handler) http.Handler {
|
func storageHandler(storageSetting setting.Storage, prefix string, objStore storage.ObjectStorage) func(next http.Handler) http.Handler {
|
||||||
|
prefix = strings.Trim(prefix, "/")
|
||||||
funcInfo := routing.GetFuncInfo(storageHandler, prefix)
|
funcInfo := routing.GetFuncInfo(storageHandler, prefix)
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
if storageSetting.ServeDirect {
|
if storageSetting.ServeDirect {
|
||||||
|
@ -37,13 +37,15 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(req.URL.RequestURI(), "/"+prefix) {
|
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
|
||||||
next.ServeHTTP(w, req)
|
next.ServeHTTP(w, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
||||||
|
|
||||||
rPath := strings.TrimPrefix(req.URL.RequestURI(), "/"+prefix)
|
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
|
||||||
|
rPath = path.Clean("/" + strings.ReplaceAll(rPath, "\\", "/"))[1:]
|
||||||
|
|
||||||
u, err := objStore.URL(rPath, path.Base(rPath))
|
u, err := objStore.URL(rPath, path.Base(rPath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
|
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
|
||||||
|
@ -55,11 +57,12 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor
|
||||||
http.Error(w, fmt.Sprintf("Error whilst getting URL for %s %s", prefix, rPath), 500)
|
http.Error(w, fmt.Sprintf("Error whilst getting URL for %s %s", prefix, rPath), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w,
|
w,
|
||||||
req,
|
req,
|
||||||
u.String(),
|
u.String(),
|
||||||
301,
|
http.StatusMovedPermanently,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -70,22 +73,18 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix := strings.Trim(prefix, "/")
|
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
|
||||||
|
|
||||||
if !strings.HasPrefix(req.URL.EscapedPath(), "/"+prefix+"/") {
|
|
||||||
next.ServeHTTP(w, req)
|
next.ServeHTTP(w, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
||||||
|
|
||||||
rPath := strings.TrimPrefix(req.URL.EscapedPath(), "/"+prefix+"/")
|
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
|
||||||
rPath = strings.TrimPrefix(rPath, "/")
|
rPath = path.Clean("/" + strings.ReplaceAll(rPath, "\\", "/"))[1:]
|
||||||
if rPath == "" {
|
if rPath == "" {
|
||||||
http.Error(w, "file not found", 404)
|
http.Error(w, "file not found", 404)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rPath = path.Clean("/" + filepath.ToSlash(rPath))
|
|
||||||
rPath = rPath[1:]
|
|
||||||
|
|
||||||
fi, err := objStore.Stat(rPath)
|
fi, err := objStore.Stat(rPath)
|
||||||
if err == nil && httpcache.HandleTimeCache(req, w, fi) {
|
if err == nil && httpcache.HandleTimeCache(req, w, fi) {
|
||||||
|
|
Reference in a new issue