Follow links safely during directory discovery
This commit is contained in:
212
internal/engine/filesystem_links.go
Normal file
212
internal/engine/filesystem_links.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
errImportDirectoryLinkCycle = errors.New("directory link cycle")
|
||||
errImportPathOutsideRoot = errors.New("resolved import path is outside the configuration root")
|
||||
)
|
||||
|
||||
// importEntryTarget describes a discovered entry without changing its lexical
|
||||
// candidate spelling. identity is resolved only to detect active traversal loops.
|
||||
type importEntryTarget struct {
|
||||
symlink bool
|
||||
dir bool
|
||||
identity string
|
||||
}
|
||||
|
||||
func (a *filesystemAccess) importEntryTarget(name string, entry fs.DirEntry, follow bool) (importEntryTarget, error) {
|
||||
if a.native == nil {
|
||||
relative, err := a.relative(name)
|
||||
if err != nil {
|
||||
return importEntryTarget{}, err
|
||||
}
|
||||
info, err := a.root.Lstat(relative)
|
||||
if err != nil {
|
||||
return importEntryTarget{}, err
|
||||
}
|
||||
symlink := info.Mode()&fs.ModeSymlink != 0
|
||||
if symlink && !follow {
|
||||
return importEntryTarget{symlink: true}, nil
|
||||
}
|
||||
if !symlink && !info.IsDir() {
|
||||
return importEntryTarget{}, nil
|
||||
}
|
||||
resolved, err := resolveHostImportPath(a, relative)
|
||||
if err != nil {
|
||||
return importEntryTarget{}, err
|
||||
}
|
||||
resolvedPath := filepath.Join(a.rootPath, filepath.FromSlash(resolved))
|
||||
info, err = a.root.Stat(filepath.FromSlash(resolved))
|
||||
if err != nil {
|
||||
return importEntryTarget{}, err
|
||||
}
|
||||
resolvedPath = filepath.Clean(resolvedPath)
|
||||
return importEntryTarget{symlink: symlink, dir: info.IsDir(), identity: resolvedPath}, nil
|
||||
}
|
||||
|
||||
linkFS, linksKnown := a.native.(fs.ReadLinkFS)
|
||||
if linksKnown {
|
||||
info, err := linkFS.Lstat(name)
|
||||
if err != nil {
|
||||
return importEntryTarget{}, err
|
||||
}
|
||||
symlink := info.Mode()&fs.ModeSymlink != 0
|
||||
if symlink && !follow {
|
||||
return importEntryTarget{symlink: true}, nil
|
||||
}
|
||||
if !symlink && !info.IsDir() {
|
||||
return importEntryTarget{}, nil
|
||||
}
|
||||
resolved, err := resolveNativeImportPath(a, linkFS, name)
|
||||
if err != nil {
|
||||
return importEntryTarget{}, err
|
||||
}
|
||||
if !a.contains(resolved) {
|
||||
return importEntryTarget{}, errImportPathOutsideRoot
|
||||
}
|
||||
info, err = fs.Stat(a.native, resolved)
|
||||
if err != nil {
|
||||
return importEntryTarget{}, err
|
||||
}
|
||||
return importEntryTarget{symlink: symlink, dir: info.IsDir(), identity: resolved}, nil
|
||||
}
|
||||
|
||||
symlink := entry != nil && entry.Type()&fs.ModeSymlink != 0
|
||||
if symlink && !follow {
|
||||
return importEntryTarget{symlink: true}, nil
|
||||
}
|
||||
if entry != nil && !symlink {
|
||||
mode := entry.Type()
|
||||
if mode == 0 {
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return importEntryTarget{}, err
|
||||
}
|
||||
mode = info.Mode()
|
||||
symlink = mode&fs.ModeSymlink != 0
|
||||
if symlink && !follow {
|
||||
return importEntryTarget{symlink: true}, nil
|
||||
}
|
||||
}
|
||||
if !symlink && !mode.IsDir() {
|
||||
return importEntryTarget{}, nil
|
||||
}
|
||||
if !symlink && mode.IsDir() {
|
||||
return importEntryTarget{dir: true, identity: name}, nil
|
||||
}
|
||||
}
|
||||
info, err := fs.Stat(a.native, name)
|
||||
if err != nil {
|
||||
return importEntryTarget{}, err
|
||||
}
|
||||
return importEntryTarget{symlink: symlink, dir: info.IsDir(), identity: name}, nil
|
||||
}
|
||||
|
||||
func resolveHostImportPath(access *filesystemAccess, name string) (string, error) {
|
||||
pending := strings.Split(filepath.ToSlash(name), "/")
|
||||
resolved := ""
|
||||
activeLinks := make(map[string]struct{})
|
||||
for len(pending) > 0 {
|
||||
segment := pending[0]
|
||||
pending = pending[1:]
|
||||
candidate := path.Join(resolved, segment)
|
||||
if candidate == "" {
|
||||
candidate = "."
|
||||
}
|
||||
info, err := access.root.Lstat(filepath.FromSlash(candidate))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if info.Mode()&fs.ModeSymlink == 0 {
|
||||
resolved = candidate
|
||||
continue
|
||||
}
|
||||
if _, exists := activeLinks[candidate]; exists {
|
||||
return "", errImportDirectoryLinkCycle
|
||||
}
|
||||
activeLinks[candidate] = struct{}{}
|
||||
target, err := access.root.Readlink(filepath.FromSlash(candidate))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if filepath.IsAbs(target) {
|
||||
return "", errImportPathOutsideRoot
|
||||
}
|
||||
targetPath := path.Clean(path.Join(path.Dir(candidate), filepath.ToSlash(target)))
|
||||
if !withinPortableRoot(targetPath) {
|
||||
return "", errImportPathOutsideRoot
|
||||
}
|
||||
remaining := append([]string(nil), pending...)
|
||||
pending = append(strings.Split(targetPath, "/"), remaining...)
|
||||
resolved = ""
|
||||
}
|
||||
if resolved == "" {
|
||||
resolved = "."
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
func withinPortableRoot(name string) bool {
|
||||
return name == "." || name != ".." && !strings.HasPrefix(name, "../") && !strings.HasPrefix(name, "/")
|
||||
}
|
||||
|
||||
func resolveNativeImportPath(access *filesystemAccess, filesystem fs.ReadLinkFS, name string) (string, error) {
|
||||
if !fs.ValidPath(name) || !access.contains(name) {
|
||||
return "", fs.ErrInvalid
|
||||
}
|
||||
pending := strings.Split(name, "/")
|
||||
resolved := ""
|
||||
activeLinks := make(map[string]struct{})
|
||||
for len(pending) > 0 {
|
||||
segment := pending[0]
|
||||
pending = pending[1:]
|
||||
candidate := path.Join(resolved, segment)
|
||||
if candidate == "" {
|
||||
candidate = "."
|
||||
}
|
||||
if !access.contains(candidate) {
|
||||
return "", errImportPathOutsideRoot
|
||||
}
|
||||
info, err := filesystem.Lstat(candidate)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if info.Mode()&fs.ModeSymlink == 0 {
|
||||
resolved = candidate
|
||||
continue
|
||||
}
|
||||
if _, exists := activeLinks[candidate]; exists {
|
||||
return "", errImportDirectoryLinkCycle
|
||||
}
|
||||
activeLinks[candidate] = struct{}{}
|
||||
target, err := filesystem.ReadLink(candidate)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if target == "" || strings.HasPrefix(target, "/") {
|
||||
return "", errImportPathOutsideRoot
|
||||
}
|
||||
targetPath := path.Clean(path.Join(path.Dir(candidate), target))
|
||||
if !fs.ValidPath(targetPath) || !access.contains(targetPath) {
|
||||
return "", errImportPathOutsideRoot
|
||||
}
|
||||
remaining := append([]string(nil), pending...)
|
||||
pending = append(strings.Split(targetPath, "/"), remaining...)
|
||||
resolved = ""
|
||||
}
|
||||
if resolved == "" {
|
||||
resolved = "."
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
func isImportLinkCycle(err error) bool {
|
||||
return errors.Is(err, errImportDirectoryLinkCycle)
|
||||
}
|
||||
Reference in New Issue
Block a user