fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
<#
|
|
|
|
|
Tests Get-NTFSSecurityDescriptor and Set-NTFSSecurityDescriptor of the module built in NTFSSecurity\bin\Release on
|
|
|
|
|
files in a sandbox folder. Tests that need the Security or the Restore privilege skip without it; CI runs them
|
|
|
|
|
elevated.
|
|
|
|
|
#>
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.'
|
|
|
|
|
)]
|
|
|
|
|
param ()
|
|
|
|
|
|
|
|
|
|
BeforeDiscovery {
|
|
|
|
|
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
|
|
|
|
|
$holdsSecurityPrivilege = Test-PrivilegeHeld -Name 'SeSecurityPrivilege'
|
|
|
|
|
# Assigning an owner other than the user or one of its groups needs the Restore privilege.
|
|
|
|
|
$canAssignAnyOwner = Test-PrivilegeHeld -Name 'SeRestorePrivilege'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BeforeAll {
|
|
|
|
|
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
|
|
|
|
|
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1'
|
|
|
|
|
Import-Module -Name $modulePath -Force -ErrorAction Stop
|
|
|
|
|
$sandbox = New-TestSandbox -Name 'SecurityDescriptor'
|
|
|
|
|
Push-Location -LiteralPath $sandbox
|
|
|
|
|
|
|
|
|
|
$privateData = (Get-Module -Name NTFSSecurity).PrivateData
|
|
|
|
|
$enablePrivileges = $privateData['EnablePrivileges']
|
|
|
|
|
$sidType = [System.Security.Principal.SecurityIdentifier]
|
|
|
|
|
# An owner that the user can assign only with the Restore privilege
|
|
|
|
|
$trustedInstaller = 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464'
|
|
|
|
|
|
|
|
|
|
function Get-RestorePrivilegeState {
|
|
|
|
|
(Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Restore').PrivilegeState
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Get-EveryoneRule ([string] $Path) {
|
|
|
|
|
(Get-Acl -LiteralPath $Path).GetAccessRules($true, $false, $sidType) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
Pop-Location
|
|
|
|
|
Remove-TestSandbox -Sandbox $sandbox
|
|
|
|
|
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Get-NTFSSecurityDescriptor' {
|
|
|
|
|
# Before 5.0.0-rc3, the cmdlet read the DACL together with the SACL when the process held the Security privilege.
|
|
|
|
|
# When the folder has no SACL, Windows then returns the inherited entries of a DACL without the auto-inherit flag,
|
|
|
|
|
# such as that of a file in the temp folder of the user, without their inherited flag.
|
|
|
|
|
It 'Should report the inherited access entries as inherited' -Skip:(-not $holdsSecurityPrivilege) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Inherited'
|
|
|
|
|
$inheritedCount = @((Get-Acl -LiteralPath $file).GetAccessRules($false, $true, $sidType)).Count
|
|
|
|
|
$inheritedCount | Should -BeGreaterThan 0
|
|
|
|
|
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
|
|
|
|
|
@($sd.SecurityDescriptor.GetAccessRules($true, $false, $sidType)) | Should -BeNullOrEmpty
|
|
|
|
|
@($sd.SecurityDescriptor.GetAccessRules($false, $true, $sidType)) | Should -HaveCount $inheritedCount
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should read the owner and the audit entries with the access entries' -Skip:(-not $holdsSecurityPrivilege) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Sections'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
Add-NTFSAudit -Path $file -Account 'Everyone' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None
|
|
|
|
|
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
|
|
|
|
|
$sd.SecurityDescriptor.GetOwner($sidType).Value | Should -Be (Get-Acl -LiteralPath $file).GetOwner($sidType).Value
|
|
|
|
|
@($sd.SecurityDescriptor.GetAuditRules($true, $false, $sidType)) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Set-NTFSSecurityDescriptor' {
|
|
|
|
|
Context 'When a cmdlet added an access entry to the descriptor' {
|
|
|
|
|
It 'Should write the entry as the only explicit entry of the item' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AddedEntry'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd
|
|
|
|
|
|
|
|
|
|
@(Get-EveryoneRule -Path $file) | Should -HaveCount 1
|
|
|
|
|
@((Get-Acl -LiteralPath $file).GetAccessRules($true, $false, $sidType)) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'When the item has an owner that the user cannot assign' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc3, the cmdlet wrote every section that Get-NTFSSecurityDescriptor read, also the unchanged
|
|
|
|
|
# owner, which Windows refuses without the Restore privilege, like a file server that refuses the owner (#34).
|
|
|
|
|
It 'Should write an added access entry and keep the owner' -Skip:(-not $canAssignAnyOwner) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'OtherOwner'
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $trustedInstaller
|
|
|
|
|
Get-RestorePrivilegeState | Should -Be 'Disabled'
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorVariable setErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$setErrors | Should -BeNullOrEmpty
|
|
|
|
|
(Get-Acl -LiteralPath $file).GetOwner($sidType).Value | Should -Be $trustedInstaller
|
|
|
|
|
@(Get-EveryoneRule -Path $file) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should write an added audit entry and keep the owner' -Skip:(-not ($holdsSecurityPrivilege -and $canAssignAnyOwner)) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'OtherOwnerAudit'
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $trustedInstaller
|
|
|
|
|
Get-RestorePrivilegeState | Should -Be 'Disabled'
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
Add-NTFSAudit -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None
|
|
|
|
|
|
|
|
|
|
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorVariable setErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$setErrors | Should -BeNullOrEmpty
|
|
|
|
|
(Get-Acl -LiteralPath $file).GetOwner($sidType).Value | Should -Be $trustedInstaller
|
|
|
|
|
@(Get-NTFSAudit -Path $file -ExcludeInherited) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'When the descriptor has sections that it did not change' {
|
|
|
|
|
# Before 5.0.0-rc3, the cmdlet wrote every section that Get-NTFSSecurityDescriptor read, so it also undid the
|
|
|
|
|
# changes that were made to the item after it was read.
|
|
|
|
|
It 'Should not write back the access entries of an unchanged descriptor' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Unchanged'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
$acl = Get-Acl -LiteralPath $file
|
|
|
|
|
$acl.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
(New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList 'S-1-1-0'),
|
|
|
|
|
[System.Security.AccessControl.FileSystemRights]::ReadData, [System.Security.AccessControl.AccessControlType]::Allow
|
|
|
|
|
)))
|
|
|
|
|
Set-Acl -LiteralPath $file -AclObject $acl
|
|
|
|
|
|
|
|
|
|
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd
|
|
|
|
|
|
|
|
|
|
@(Get-EveryoneRule -Path $file) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should write the owner when only the owner changed' -Skip:(-not $canAssignAnyOwner) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'NewOwner'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
$sd.SecurityDescriptor.SetOwner((New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList $trustedInstaller))
|
|
|
|
|
|
|
|
|
|
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd
|
|
|
|
|
|
|
|
|
|
(Get-Acl -LiteralPath $file).GetOwner($sidType).Value | Should -Be $trustedInstaller
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'With -Verbose' {
|
|
|
|
|
It 'Should name the sections that it writes' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'VerboseChanged'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
$messages = Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -Verbose 4>&1
|
|
|
|
|
|
|
|
|
|
$messages.Message | Should -Contain "Writing the changed sections of the security descriptor of '$($sd.FullName)': Access"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should say that it writes nothing for an unchanged descriptor' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'VerboseUnchanged'
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
|
|
|
|
|
$messages = Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -Verbose 4>&1
|
|
|
|
|
|
|
|
|
|
$messages.Message |
|
|
|
|
|
Should -Contain "No section of the security descriptor of '$($sd.FullName)' changed since it was read or last written; nothing is written"
|
|
|
|
|
}
|
|
|
|
|
}
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
}
|