fix: harden workspace preprocessing contract

This commit is contained in:
2026-08-11 00:59:15 +02:00
parent 9df7c38069
commit 05a6e8cc2d
11 changed files with 494 additions and 32 deletions
@@ -122,7 +122,14 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
if err := ValidateCanonicalPath(path); err != nil || len(contents) > 16<<20 || mode.Perm() == 0 || mode.Perm()&0o077 != 0 {
return ErrUnsafeFile
}
h, err := windows.CreateFile(windows.StringToUTF16Ptr(path), windows.GENERIC_WRITE, 0, nil, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
parent, retainedParents, err := openWindowsParents(path)
if err != nil {
return ErrUnsafeFile
}
defer closeWindowsHandles(retainedParents)
// Parent handles stay open with delete sharing denied until publication and
// identity recheck complete; this is the Windows equivalent of retained dirfds.
h, err := windows.CreateFile(windows.StringToUTF16Ptr(filepath.Join(parent, filepath.Base(path))), windows.GENERIC_WRITE, 0, nil, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
if err != nil {
return ErrUnsafeFile
}
@@ -132,6 +139,9 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
return ErrUnsafeFile
}
defer f.Close()
if err := f.Chmod(mode); err != nil {
return ErrUnsafeFile
}
if _, err := f.Write(contents); err != nil {
return ErrUnsafeFile
}
@@ -153,26 +163,37 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
return nil
}
func validateCanonicalOutputPath(path string) error {
if err := ValidateCanonicalPath(path); err != nil {
return err
}
func openWindowsParents(path string) (string, []windows.Handle, error) {
volume := filepath.VolumeName(path)
root := volume + string(filepath.Separator)
components := strings.Split(strings.TrimPrefix(path, root), string(filepath.Separator))
if volume == "" || len(components) < 2 || components[0] == "" {
return ErrUnsafeFile
return "", nil, ErrUnsafeFile
}
current := root
parents := make([]windows.Handle, 0, len(components)-1)
for _, component := range components[:len(components)-1] {
current = filepath.Join(current, component)
h, err := openWindowsComponent(current, true)
if err != nil {
return ErrUnsafeFile
closeWindowsHandles(parents)
return "", nil, ErrUnsafeFile
}
windows.CloseHandle(h)
parents = append(parents, h)
}
if _, err := os.Lstat(filepath.Join(current, components[len(components)-1])); err == nil || !os.IsNotExist(err) {
return current, parents, nil
}
func validateCanonicalOutputPath(path string) error {
if err := ValidateCanonicalPath(path); err != nil {
return err
}
current, parents, err := openWindowsParents(path)
if err != nil {
return ErrUnsafeFile
}
defer closeWindowsHandles(parents)
if _, err := os.Lstat(filepath.Join(current, filepath.Base(path))); err == nil || !os.IsNotExist(err) {
return ErrUnsafeFile
}
return nil