Files
goclaw/internal/tools/filesystem_windows.go
T
badgerbeesandviettranx 551b6670d5 fix(tools): resolve Windows build errors and harden filesystem security (#318)
* fix(tools): resolve Windows build errors and harden filesystem security

* fix(tools): remove stale Windows skip in hardlink test

Now that checkHardlink uses GetFileInformationByHandle on Windows,
the test should run on all platforms.

---------

Co-authored-by: viettranx <viettranx@gmail.com>
2026-03-21 07:29:48 +07:00

106 lines
2.5 KiB
Go

//go:build windows
package tools
import (
"fmt"
"log/slog"
"os"
"path/filepath"
"syscall"
)
// hasMutableSymlinkParent checks if any component of the resolved path is a symlink
// whose parent directory is writable by the current process. A writable parent means
// the symlink could be replaced between path resolution and actual file operation
// (TOCTOU symlink rebind attack).
func hasMutableSymlinkParent(path string) bool {
clean := filepath.Clean(path)
// Traverse from leaf up to root to find all path components.
var components []string
curr := clean
for {
components = append([]string{curr}, components...)
parent := filepath.Dir(curr)
if parent == curr {
break
}
curr = parent
}
for _, p := range components {
info, err := os.Lstat(p)
if err != nil {
break // non-existent — stop checking
}
if info.Mode()&os.ModeSymlink != 0 {
// Symlink found — check if its parent dir is writable
if isDirWritable(filepath.Dir(p)) {
return true
}
}
}
return false
}
// isDirWritable checks if a directory is writable by attempting to open it with write access.
// On Windows, this is more robust than temporary file creation and avoids artifact leakage.
func isDirWritable(dir string) bool {
dirUTF16, err := syscall.UTF16PtrFromString(dir)
if err != nil {
return false
}
// FILE_FLAG_BACKUP_SEMANTICS is required to open a handle to a directory.
h, err := syscall.CreateFile(
dirUTF16,
syscall.GENERIC_WRITE,
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
nil,
syscall.OPEN_EXISTING,
syscall.FILE_FLAG_BACKUP_SEMANTICS,
0)
if err != nil {
return false
}
syscall.CloseHandle(h)
return true
}
// checkHardlink rejects regular files with NumberOfLinks > 1 (hardlink attack prevention).
func checkHardlink(path string) error {
info, err := os.Lstat(path)
if err != nil || info.IsDir() {
return nil
}
pathUTF16, err := syscall.UTF16PtrFromString(path)
if err != nil {
return nil
}
h, err := syscall.CreateFile(
pathUTF16,
syscall.GENERIC_READ,
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
nil,
syscall.OPEN_EXISTING,
syscall.FILE_FLAG_BACKUP_SEMANTICS,
0)
if err != nil {
return nil
}
defer syscall.CloseHandle(h)
var fi syscall.ByHandleFileInformation
if err := syscall.GetFileInformationByHandle(h, &fi); err != nil {
return nil
}
if fi.NumberOfLinks > 1 {
slog.Warn("security.hardlink_rejected", "path", path, "nlink", fi.NumberOfLinks)
return fmt.Errorf("access denied: hardlinked file not allowed")
}
return nil
}