fix(thothctl): harden workspace result and candidate publication
This commit is contained in:
@@ -186,8 +186,11 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
if err := stageFile.Sync(); err != nil {
|
||||
return fail()
|
||||
}
|
||||
// The stage name is visible on Win32. If a same-user actor hard-links it,
|
||||
// the bytes are already public; do not turn that observation into a failure
|
||||
// whose cleanup could not remove the attacker's link.
|
||||
var afterWrite windows.ByHandleFileInformation
|
||||
if err := windows.GetFileInformationByHandle(stageHandle, &afterWrite); err != nil || afterWrite.NumberOfLinks != 1 || afterWrite.FileSizeHigh != uint32(uint64(len(contents))>>32) || afterWrite.FileSizeLow != uint32(len(contents)) {
|
||||
if err := windows.GetFileInformationByHandle(stageHandle, &afterWrite); err != nil || afterWrite.NumberOfLinks == 0 || afterWrite.FileSizeHigh != uint32(uint64(len(contents))>>32) || afterWrite.FileSizeLow != uint32(len(contents)) {
|
||||
return fail()
|
||||
}
|
||||
if err := stageFile.Close(); err != nil {
|
||||
@@ -203,16 +206,15 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
published = true
|
||||
publishedIdentity = staged
|
||||
check, identityErr := windowsFileIdentity(finalPath)
|
||||
if identityErr != nil || check.NumberOfLinks != 2 || !sameWindowsFile(staged, check) {
|
||||
if identityErr != nil || check.NumberOfLinks < 2 || !sameWindowsFile(staged, check) {
|
||||
return fail()
|
||||
}
|
||||
publishedIdentity = check
|
||||
if err := windows.DeleteFile(windows.StringToUTF16Ptr(stagePath)); err != nil {
|
||||
return fail()
|
||||
}
|
||||
stageCreated = false
|
||||
finalIdentity, identityErr := windowsFileIdentity(finalPath)
|
||||
if identityErr != nil || finalIdentity.NumberOfLinks != 1 || !sameWindowsFile(staged, finalIdentity) {
|
||||
if identityErr != nil || finalIdentity.NumberOfLinks == 0 || !sameWindowsFile(staged, finalIdentity) {
|
||||
return fail()
|
||||
}
|
||||
return nil
|
||||
@@ -292,13 +294,39 @@ func validatePlatformPathSyntax(path string) error {
|
||||
}
|
||||
volume := filepath.VolumeName(path)
|
||||
for _, component := range strings.Split(strings.TrimPrefix(path, volume+string(filepath.Separator)), string(filepath.Separator)) {
|
||||
if strings.Contains(component, ":") {
|
||||
if strings.Contains(component, ":") || strings.HasSuffix(component, " ") || strings.HasSuffix(component, ".") || isWindowsDeviceComponent(component) {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Win32 aliases these names to devices, even when an extension is appended.
|
||||
// Rejecting them lexically is required because CreateFile does not promise an
|
||||
// independent regular leaf for a path containing one of these components.
|
||||
func isWindowsDeviceComponent(component string) bool {
|
||||
base := strings.ToUpper(component)
|
||||
if i := strings.IndexByte(base, '.'); i >= 0 {
|
||||
base = base[:i]
|
||||
}
|
||||
switch base {
|
||||
case "CON", "PRN", "AUX", "NUL", "CONIN$", "CONOUT$":
|
||||
return true
|
||||
}
|
||||
if len(base) == 4 && (strings.HasPrefix(base, "COM") || strings.HasPrefix(base, "LPT")) {
|
||||
if base[3] >= '1' && base[3] <= '9' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Unicode superscript 1, 2 and 3 are accepted as COM/LPT suffixes by
|
||||
// Win32's device-name compatibility rules.
|
||||
if len([]rune(base)) == 4 && (strings.HasPrefix(base, "COM") || strings.HasPrefix(base, "LPT")) {
|
||||
suffix := []rune(base)[3]
|
||||
return suffix == '¹' || suffix == '²' || suffix == '³'
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sameWindowsFile(a, b windows.ByHandleFileInformation) bool {
|
||||
return a.VolumeSerialNumber == b.VolumeSerialNumber && a.FileIndexHigh == b.FileIndexHigh && a.FileIndexLow == b.FileIndexLow
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user