mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-04 20:13:25 +00:00
* 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>
106 lines
2.5 KiB
Go
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
|
|
}
|