99 lines
2.2 KiB
Go
Executable File
99 lines
2.2 KiB
Go
Executable File
package files
|
|
|
|
import (
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
maxSubfolderDepth = 4
|
|
maxNameBytes = 255
|
|
)
|
|
|
|
var ErrUnsafePath = errors.New("path is not allowed")
|
|
|
|
func ResolveInside(root, relative string) (string, error) {
|
|
cleaned := filepath.Clean(filepath.FromSlash(relative))
|
|
if relative == "" || cleaned == "." || !filepath.IsLocal(cleaned) {
|
|
return "", ErrUnsafePath
|
|
}
|
|
|
|
resolvedRoot, err := filepath.EvalSymlinks(root)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
candidate := filepath.Join(resolvedRoot, cleaned)
|
|
info, err := os.Lstat(candidate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if info.Mode()&fs.ModeSymlink != 0 {
|
|
return "", ErrUnsafePath
|
|
}
|
|
|
|
resolved, err := filepath.EvalSymlinks(candidate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !IsWithin(resolvedRoot, resolved) {
|
|
return "", ErrUnsafePath
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
func IsWithin(root, target string) bool {
|
|
relative, err := filepath.Rel(root, target)
|
|
return err == nil && relative != "." && filepath.IsLocal(relative)
|
|
}
|
|
|
|
func SanitizeSubfolder(raw string) (string, error) {
|
|
trimmed := strings.Trim(strings.TrimSpace(raw), "/")
|
|
if trimmed == "" {
|
|
return "", nil
|
|
}
|
|
|
|
parts := strings.Split(trimmed, "/")
|
|
if len(parts) > maxSubfolderDepth {
|
|
return "", ErrUnsafePath
|
|
}
|
|
for index, part := range parts {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" || part == "." || part == ".." || len(part) > maxNameBytes || strings.ContainsFunc(part, isUnsafeRune) {
|
|
return "", ErrUnsafePath
|
|
}
|
|
parts[index] = part
|
|
}
|
|
return filepath.Join(parts...), nil
|
|
}
|
|
|
|
func EnsureSubdirectory(root, subfolder string) (string, error) {
|
|
current := root
|
|
if subfolder == "" {
|
|
return current, nil
|
|
}
|
|
|
|
for _, part := range strings.Split(subfolder, string(filepath.Separator)) {
|
|
current = filepath.Join(current, part)
|
|
info, err := os.Lstat(current)
|
|
switch {
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
if err := os.Mkdir(current, 0o755); err != nil && !errors.Is(err, fs.ErrExist) {
|
|
return "", err
|
|
}
|
|
case err != nil:
|
|
return "", err
|
|
case !info.IsDir():
|
|
return "", ErrUnsafePath
|
|
}
|
|
}
|
|
return current, nil
|
|
}
|
|
|
|
func isUnsafeRune(character rune) bool {
|
|
return character < 0x20 || character == 0x7f || character == '\\'
|
|
}
|