Browse Source

fix(privileges): report a malformed EnablePrivileges setting with one clear error

BaseCmdletWithPrivControl.BeginProcessing and EndProcessing read
(bool)privateData["EnablePrivileges"] without checking it first; a
setting that is not a Boolean, such as the string '$true' from a typo
in the module manifest, or a missing key, made every cmdlet derived
from the class fail with an InvalidCastException or a
NullReferenceException before anything useful was said.
Enable-Privileges had its own try/catch for the same cast that raised a
different message (a ParseException), but it was unreachable, because
BeginProcessing always failed first.

A new helper, GetEnablePrivilegesSetting, centralizes the check in the
base class: a setting that is missing or not a Boolean now stops the
cmdlet with one terminating error, id EnablePrivilegesSettingInvalid,
category InvalidData, that names the setting, says it must be $true or
$false, and names the module manifest. BeginProcessing, EndProcessing,
and Enable-Privileges all use it now; the dead try/catch of
Enable-Privileges is removed, so a mistaken setting no longer produces
two different messages depending on which cmdlet hit it first. The
valid settings $true and $false keep today's behavior.

GetInheritedFrom (used by Get-NTFSAccess and Get-NTFSAudit),
GetFileSystemModeProperty, and IdentifyHardLinks (both used by
Get-ChildItem2) read their own setting the same unchecked way and share
this weakness; they are not changed here; see the final report.

Breaking: a script that caught the old InvalidCastException,
NullReferenceException, or ParseException for a malformed
EnablePrivileges setting now needs to catch the terminating error with
id EnablePrivilegesSettingInvalid instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
pull/131/head
Raimund Andree 2 weeks ago
parent
commit
b69b21e14b
  1. 6
      Docs/Concepts.md
  2. 35
      NTFSSecurity/BaseCmdlets.cs
  3. 13
      NTFSSecurity/OtherCmdlets.cs
  4. 37
      Tests/Privileges.Tests.ps1

6
Docs/Concepts.md

@ -274,6 +274,12 @@ change the module's behavior:
cost extra work for every item. Turn them off to speed up large folder cost extra work for every item. Turn them off to speed up large folder
trees when you don't need the information. trees when you don't need the information.
A malformed `EnablePrivileges` setting, such as a string instead of
`$true` or `$false`, or a missing key, stops every cmdlet that uses
privileges, and `Enable-Privileges`, with the error
`EnablePrivilegesSettingInvalid` naming the setting, instead of an
unrelated exception.
To change a setting for the current session only, change the value after To change a setting for the current session only, change the value after
you import the module: you import the module:

35
NTFSSecurity/BaseCmdlets.cs

@ -354,7 +354,6 @@ namespace NTFSSecurity
protected PrivilegeAndAttributesCollection privileges = null; protected PrivilegeAndAttributesCollection privileges = null;
protected PrivilegeControl privControl = new PrivilegeControl(); protected PrivilegeControl privControl = new PrivilegeControl();
private List<string> enabledPrivileges = new List<string>(); private List<string> enabledPrivileges = new List<string>();
Hashtable privateData = null;
// A cmdlet that enables the privileges for the session, such as Enable-Privileges, keeps them enabled. // A cmdlet that enables the privileges for the session, such as Enable-Privileges, keeps them enabled.
protected virtual bool KeepEnabledPrivileges protected virtual bool KeepEnabledPrivileges
@ -364,9 +363,7 @@ namespace NTFSSecurity
protected override void BeginProcessing() protected override void BeginProcessing()
{ {
privateData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData; if (GetEnablePrivilegesSetting())
if ((bool)privateData["EnablePrivileges"])
{ {
WriteVerbose("EnablePrivileges enabled in PrivateDate"); WriteVerbose("EnablePrivileges enabled in PrivateDate");
EnableFileSystemPrivileges(true); EnableFileSystemPrivileges(true);
@ -375,7 +372,7 @@ namespace NTFSSecurity
protected override void EndProcessing() protected override void EndProcessing()
{ {
if ((bool)privateData["EnablePrivileges"]) if (GetEnablePrivilegesSetting())
{ {
WriteVerbose("EnablePrivileges enabled in PrivateDate"); WriteVerbose("EnablePrivileges enabled in PrivateDate");
@ -395,6 +392,34 @@ namespace NTFSSecurity
} }
} }
/// <summary>
/// Reads the EnablePrivileges setting of the module manifest (NTFSSecurity.psd1, section PrivateData) and
/// terminates with one EnablePrivilegesSettingInvalid error when the setting is missing or is not a Boolean.
/// Before 5.0.0, BeginProcessing, EndProcessing, and Enable-Privileges each cast the setting on their own
/// without checking it first, so a missing key or a value that was not a Boolean, such as the string '$true'
/// from a typo in the manifest, failed with a NullReferenceException or an InvalidCastException before
/// anything useful was said, and Enable-Privileges raised a second, different message for the same cause
/// that was never reached, because BeginProcessing always failed first.
/// </summary>
protected bool GetEnablePrivilegesSetting()
{
var moduleData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData;
var value = moduleData == null ? null : moduleData["EnablePrivileges"];
if (!(value is bool))
{
var found = value == null ? "missing" : string.Format("'{0}'", value);
var message = string.Format(
"The 'EnablePrivileges' setting in the PrivateData section of the module manifest (NTFSSecurity.psd1) " +
"must be $true or $false, but it is {0}. See the NTFSSecurity documentation for EnablePrivileges.",
found);
ThrowTerminatingError(new ErrorRecord(new ArgumentException(message), "EnablePrivilegesSettingInvalid", ErrorCategory.InvalidData, value));
}
return (bool)value;
}
// PowerShell calls Dispose also when a later command or a terminating error stops the pipeline, and then // PowerShell calls Dispose also when a later command or a terminating error stops the pipeline, and then
// skips EndProcessing. Before 5.0.0-rc6, the privileges that the cmdlet had enabled stayed enabled in the // skips EndProcessing. Before 5.0.0-rc6, the privileges that the cmdlet had enabled stayed enabled in the
// session in that case. PowerShell ignores an exception from Dispose and no stream is open anymore, so a // session in that case. PowerShell ignores an exception from Dispose and no stream is open anymore, so a

13
NTFSSecurity/OtherCmdlets.cs

@ -29,17 +29,12 @@ namespace NTFSSecurity
protected override void ProcessRecord() protected override void ProcessRecord()
{ {
var privateData = (System.Collections.Hashtable)this.MyInvocation.MyCommand.Module.PrivateData;
var psCallStack = (CallStackFrame)this.InvokeCommand.InvokeScript("Get-PSCallStack")[1].BaseObject; var psCallStack = (CallStackFrame)this.InvokeCommand.InvokeScript("Get-PSCallStack")[1].BaseObject;
try // The base class raises one EnablePrivilegesSettingInvalid error for a setting that is not $true or
{ // $false; before 5.0.0, this had its own try/catch that raised a ParseException for the same cast, but
enablePrivileges = (bool)privateData["EnablePrivileges"]; // it never ran, because BeginProcessing always failed first with its own uncaught cast.
} enablePrivileges = GetEnablePrivilegesSetting();
catch (Exception ex)
{
throw new ParseException("Could not parse the module's PrivateData field in the module's psd1 file. Please refer to the documentation for further details", ex);
}
//if the command is called from NTFSSecurity.Init.ps1 and EnablePrivileges is set to true in the NTFSSecurity.psd1 or if the cmdlet is called from somewhere else //if the command is called from NTFSSecurity.Init.ps1 and EnablePrivileges is set to true in the NTFSSecurity.psd1 or if the cmdlet is called from somewhere else
if ((psCallStack.InvocationInfo.MyCommand.Name == "NTFSSecurity.Init.ps1" && enablePrivileges == true)) if ((psCallStack.InvocationInfo.MyCommand.Name == "NTFSSecurity.Init.ps1" && enablePrivileges == true))

37
Tests/Privileges.Tests.ps1

@ -663,3 +663,40 @@ Describe 'The PrivilegeControl class' {
Get-BackupPrivilegeState | Should -Be 'Disabled' Get-BackupPrivilegeState | Should -Be 'Disabled'
} }
} }
# Before 5.0.0, a module setting that was not a Boolean (a typo such as the string '$true', or a missing key) made
# every cmdlet derived from BaseCmdletWithPrivControl fail in BeginProcessing with an InvalidCastException or a
# NullReferenceException before anything useful was said, and Enable-Privileges had its own try/catch for the same
# cast that never ran, because BeginProcessing always failed first. One EnablePrivilegesSettingInvalid error now
# names the setting instead, for a cmdlet and for Enable-Privileges alike. The test needs no elevation.
Describe 'A malformed EnablePrivileges setting' {
BeforeAll {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'MalformedSetting'
}
It 'Should stop a cmdlet and Enable-Privileges with one EnablePrivilegesSettingInvalid error for <Case>, not an InvalidCastException or a NullReferenceException' -ForEach @(
@{ Case = 'a string value'; Setting = 'maybe' }
@{ Case = 'a missing value'; Setting = $null }
) {
$privateData['EnablePrivileges'] = $Setting
try {
$cmdletFailure = { Get-NTFSInheritance -Path $file -ErrorAction Stop } | Should -Throw -PassThru
$enableFailure = { Enable-Privileges -ErrorAction Stop } | Should -Throw -PassThru
}
finally {
$privateData['EnablePrivileges'] = $enablePrivileges
}
foreach ($failure in $cmdletFailure, $enableFailure) {
$failure.FullyQualifiedErrorId | Should -BeLike 'EnablePrivilegesSettingInvalid,*'
$failure.Exception.Message | Should -BeLike '*EnablePrivileges*'
$failure.Exception | Should -Not -BeOfType [System.InvalidCastException]
$failure.Exception | Should -Not -BeOfType [System.NullReferenceException]
}
# Restoring the setting must leave the session usable again, for this test and the ones that follow it.
$privateData['EnablePrivileges'] | Should -Be $enablePrivileges
{ Get-NTFSInheritance -Path $file -ErrorAction Stop } | Should -Not -Throw
}
}

Loading…
Cancel
Save