Files
ddc/internal/files/paths.go
T

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 == '\\'
}