fix(auth): close diagnostic filesystem races

This commit is contained in:
2026-08-17 13:04:15 +02:00
parent be1724890a
commit 6d438f4c7e
16 changed files with 545 additions and 90 deletions
@@ -107,3 +107,55 @@ func TestReplaceCanonicalRegularRejectsSymlinkedPathComponents(t *testing.T) {
t.Fatalf("final symlink replacement error = %v, want ErrUnsafeFile", err)
}
}
func TestPrivateDirectoryCreationUsesThePinnedParentAfterAncestorSwap(t *testing.T) {
temporaryRoot, err := filepath.EvalSymlinks(os.TempDir())
if err != nil {
t.Fatal(err)
}
root, err := os.MkdirTemp(temporaryRoot, "tht-safeio-mkdirat-")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.RemoveAll(root) })
for _, target := range []string{"root", "sessions", "oidc"} {
t.Run(target, func(t *testing.T) {
caseRoot := filepath.Join(root, target)
parent := filepath.Join(caseRoot, "parent")
if err := os.MkdirAll(parent, 0o700); err != nil {
t.Fatal(err)
}
path := filepath.Join(parent, "auth")
swappedAncestor := parent
if target != "root" {
if err := os.Mkdir(path, 0o700); err != nil {
t.Fatal(err)
}
swappedAncestor = path
path = filepath.Join(path, target)
}
outside := filepath.Join(caseRoot, "outside")
if err := os.Mkdir(outside, 0o700); err != nil {
t.Fatal(err)
}
movedAncestor := swappedAncestor + "-original"
if err := createPrivateDirectoryAfterParentOpen(path, func() {
if err := os.Rename(swappedAncestor, movedAncestor); err != nil {
t.Fatal(err)
}
if err := os.Symlink(outside, swappedAncestor); err != nil {
t.Fatal(err)
}
}); err != nil {
t.Fatal(err)
}
if err := ValidatePrivateDirectory(filepath.Join(movedAncestor, filepath.Base(path))); err != nil {
t.Fatalf("pinned-parent creation failed: %v", err)
}
if _, err := os.Lstat(filepath.Join(outside, filepath.Base(path))); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("outside target was mutated: %v", err)
}
})
}
}
+17
View File
@@ -11,11 +11,28 @@ import (
)
func createPrivateDirectory(path string) error {
return createPrivateDirectoryAfterParentOpen(path, nil)
}
func createPrivateDirectoryAfterParentOpen(path string, afterOpen func()) error {
parents, err := openCanonicalUnixParent(path)
if err != nil {
return ErrUnsafeFile
}
defer parents.Close()
if afterOpen != nil {
afterOpen()
}
return createPrivateDirectoryAt(parents)
}
// createPrivateDirectoryAt performs every mutating operation relative to the already-opened
// parent. An attacker can rename or replace any lexical ancestor after the open without
// redirecting mkdir or chmod into a different directory tree.
func createPrivateDirectoryAt(parents *unixParentHandles) error {
if parents == nil || parents.parent < 0 || parents.target == "" {
return ErrUnsafeFile
}
if err := unix.Mkdirat(parents.parent, parents.target, 0o700); err != nil {
if errors.Is(err, unix.EEXIST) {
return os.ErrExist