fix(auth): close diagnostic filesystem races
This commit is contained in:
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user