Follow links safely during directory discovery

This commit is contained in:
2026-10-19 20:29:20 +00:00
parent f69fa24b40
commit 9b99463187
4 changed files with 602 additions and 24 deletions

View File

@@ -6,6 +6,7 @@ import (
"path"
"path/filepath"
"sort"
"strings"
)
type importSelectionOutcome uint8
@@ -106,6 +107,10 @@ func selectDirectoryImportCandidates(access *filesystemAccess, basePath string,
if !isDirectory {
return importSelection{}, []Diagnostic{*importSelectionDiagnostic(root.site, CodeImportInvalid, "directory search root is not a directory")}
}
rootTarget, targetErr := access.importEntryTarget(root.accessPath, nil, true)
if targetErr != nil {
return importSelection{}, []Diagnostic{*importDirectoryTargetDiagnostic(root.site, targetErr)}
}
entries, missing, diagnostic := importDirectoryEntries(access, root.accessPath, root.site, limits, accounting)
if diagnostic != nil {
return importSelection{}, []Diagnostic{*diagnostic}
@@ -113,11 +118,13 @@ func selectDirectoryImportCandidates(access *filesystemAccess, basePath string,
if missing {
continue
}
stack := []importDirectoryFrame{{directory: root, depth: 0, entries: entries}}
stack := []importDirectoryFrame{{directory: root, depth: 0, entries: entries, identity: rootTarget.identity}}
activeDirectories := map[string]struct{}{rootTarget.identity: {}}
for len(stack) != 0 {
frameIndex := len(stack) - 1
frame := &stack[frameIndex]
if frame.next >= len(frame.entries) {
delete(activeDirectories, frame.identity)
stack = stack[:frameIndex]
continue
}
@@ -127,14 +134,26 @@ func selectDirectoryImportCandidates(access *filesystemAccess, basePath string,
candidate := selectedImportCandidate{
path: normalizedImportCandidatePath(access, accessPath), accessPath: accessPath, site: frame.directory.site,
}
isDirectory, entryErr := importDirectoryEntryIsDirectory(entry)
if entryErr != nil {
return importSelection{}, []Diagnostic{*importSelectionDiagnostic(frame.directory.site, CodeImportInvalid, "directory entry could not be inspected")}
if !policy.hidden && strings.HasPrefix(entry.Name(), ".") {
continue
}
if isDirectory {
target, entryErr := access.importEntryTarget(candidate.accessPath, entry, policy.follow)
if entryErr != nil {
if errors.Is(entryErr, fs.ErrNotExist) {
continue
}
return importSelection{}, []Diagnostic{*importDirectoryTargetDiagnostic(frame.directory.site, entryErr)}
}
if target.symlink && !policy.follow {
continue
}
if target.dir {
if !directoryDepthAllows(policy, frame.depth) {
continue
}
if _, exists := activeDirectories[target.identity]; exists {
return importSelection{}, []Diagnostic{*importSelectionDiagnostic(frame.directory.site, CodeImportCycle, "directory traversal cycle detected")}
}
childMissing, childIsDirectory, childDiagnostic := validateNamedImportTarget(access, candidate, false, true)
if childDiagnostic != nil {
return importSelection{}, []Diagnostic{*childDiagnostic}
@@ -147,7 +166,8 @@ func selectDirectoryImportCandidates(access *filesystemAccess, basePath string,
return importSelection{}, []Diagnostic{*childDiagnostic}
}
if !vanished {
stack = append(stack, importDirectoryFrame{directory: candidate, depth: frame.depth + 1, entries: childEntries})
activeDirectories[target.identity] = struct{}{}
stack = append(stack, importDirectoryFrame{directory: candidate, depth: frame.depth + 1, entries: childEntries, identity: target.identity})
}
continue
}
@@ -188,6 +208,17 @@ type importDirectoryFrame struct {
depth int
entries []fs.DirEntry
next int
identity string
}
func importDirectoryTargetDiagnostic(site importFieldSite, err error) *Diagnostic {
if isImportLinkCycle(err) {
return importSelectionDiagnostic(site, CodeImportCycle, "directory traversal cycle detected")
}
if errors.Is(err, errImportPathOutsideRoot) {
return importSelectionDiagnostic(site, CodeImportOutsideRoot, "import target resolves outside the configuration root")
}
return importSelectionDiagnostic(site, CodeImportInvalid, "directory entry could not be inspected")
}
func importDirectoryEntries(access *filesystemAccess, directory string, site importFieldSite, limits importLimits, accounting *importAccounting) ([]fs.DirEntry, bool, *Diagnostic) {