Permalink
Fetching contributors…
Cannot retrieve contributors at this time
780 lines (676 sloc) 31 KB
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Dbg = System.Management.Automation;
using System;
using System.Management.Automation;
using System.Management.Automation.Security;
using System.Management.Automation.Internal;
using System.IO;
using System.Collections.ObjectModel;
using System.Management.Automation.Host;
using System.Management.Automation.Language;
using System.Security;
using System.Security.Cryptography.X509Certificates;
namespace Microsoft.PowerShell
{
/// <summary>
/// Defines the authorization policy that controls the way scripts
/// (and other command types) are handled by Monad. This authorization
/// policy enforces one of four levels, as defined by the 'ExecutionPolicy'
/// value in one of the following locations:
///
/// In priority-order (highest priority first,) these come from:
///
/// - Machine-wide Group Policy
/// HKLM\Software\Policies\Microsoft\Windows\PowerShell
/// - Current-user Group Policy
/// HKCU\Software\Policies\Microsoft\Windows\PowerShell.
/// - Current session preference
/// ENV:PSExecutionPolicyPreference
/// - Current user machine preference
/// HKEY_CURRENT_USER\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell
/// - Local machine preference
/// HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell
///
/// Restricted - All .ps1 files are blocked. ps1xml files must be digitally
/// signed, and by a trusted publisher. If you haven't made a trust decision
/// on the publisher yet, prompting is done as in AllSigned mode.
/// AllSigned - All .ps1 and .ps1xml files must be digitally signed. If
/// signed and executed, Monad prompts to determine if files from the
/// signing publisher should be run or not.
/// RemoteSigned - Only .ps1 and .ps1xml files originating from the internet
/// must be digitally signed. If remote, signed, and executed, Monad
/// prompts to determine if files from the signing publisher should be
/// run or not. This is the default setting.
/// Unrestricted - No files must be signed. If a file originates from the
/// internet, Monad provides a warning prompt to alert the user. To
/// suppress this warning message, right-click on the file in File Explorer,
/// select "Properties," and then "Unblock." Requires Shell.
/// Bypass - No files must be signed, and internet origin is not verified
/// </summary>
public sealed class PSAuthorizationManager : AuthorizationManager
{
internal enum RunPromptDecision
{
NeverRun = 0,
DoNotRun = 1,
RunOnce = 2,
AlwaysRun = 3,
Suspend = 4
}
#region constructor
// execution policy that dictates what can run in msh
private ExecutionPolicy _executionPolicy;
//shellId supplied by runspace configuration
private string _shellId;
/// <summary>
/// Initializes a new instance of the PSAuthorizationManager
/// class, for a given ShellId.
/// </summary>
/// <param name="shellId">
/// The shell identifier that the authorization manager applies
/// to. For example, Microsoft.PowerShell
/// </param>
public PSAuthorizationManager(string shellId)
: base(shellId)
{
if (string.IsNullOrEmpty(shellId))
{
throw PSTraceSource.NewArgumentNullException("shellId");
}
_shellId = shellId;
}
#endregion constructor
#region signing check
private static bool IsSupportedExtension(string ext)
{
return (
ext.Equals(".ps1", StringComparison.OrdinalIgnoreCase) ||
ext.Equals(".ps1xml", StringComparison.OrdinalIgnoreCase) ||
ext.Equals(".psm1", StringComparison.OrdinalIgnoreCase) ||
ext.Equals(".psd1", StringComparison.OrdinalIgnoreCase) ||
ext.Equals(".xaml", StringComparison.OrdinalIgnoreCase) ||
ext.Equals(".cdxml", StringComparison.OrdinalIgnoreCase));
}
private bool CheckPolicy(ExternalScriptInfo script, PSHost host, out Exception reason)
{
bool policyCheckPassed = false;
reason = null;
string path = script.Path;
string reasonMessage;
// path is assumed to be fully qualified here
if (path.IndexOf(System.IO.Path.DirectorySeparatorChar) < 0)
{
throw PSTraceSource.NewArgumentException("path");
}
if (path.LastIndexOf(System.IO.Path.DirectorySeparatorChar) == (path.Length - 1))
{
throw PSTraceSource.NewArgumentException("path");
}
FileInfo fi = new FileInfo(path);
// Return false if the file does not exist, so that
// we don't introduce a race condition
if (!fi.Exists)
{
reason = new FileNotFoundException(path);
return false;
}
// Quick exit if we don't support the file type
if (!IsSupportedExtension(fi.Extension))
return true;
// Get the execution policy
_executionPolicy = SecuritySupport.GetExecutionPolicy(_shellId);
// See if they want to bypass the authorization manager
if (_executionPolicy == ExecutionPolicy.Bypass)
return true;
// Always check the SAFER APIs if code integrity isn't being handled system-wide through
// WLDP or AppLocker. In those cases, the scripts will be run in ConstrainedLanguage.
// Otherwise, block.
// SAFER APIs are not on CSS or OneCore
if (SystemPolicy.GetSystemLockdownPolicy() != SystemEnforcementMode.Enforce)
{
SaferPolicy saferPolicy = SaferPolicy.Disallowed;
int saferAttempt = 0;
bool gotSaferPolicy = false;
// We need to put in a retry workaround, as the SAFER APIs fail when under stress.
while ((!gotSaferPolicy) && (saferAttempt < 5))
{
try
{
saferPolicy = SecuritySupport.GetSaferPolicy(path, null);
gotSaferPolicy = true;
}
catch (System.ComponentModel.Win32Exception)
{
if (saferAttempt > 4) { throw; }
saferAttempt++;
System.Threading.Thread.Sleep(100);
}
}
// If the script is disallowed via AppLocker, block the file
// unless the system-wide lockdown policy is "Enforce" (where all PowerShell
// scripts are in blocked). If the system policy is "Enforce", then the
// script will be allowed (but ConstrainedLanguage will be applied).
if (saferPolicy == SaferPolicy.Disallowed)
{
reasonMessage = StringUtil.Format(Authenticode.Reason_DisallowedBySafer, path);
reason = new UnauthorizedAccessException(reasonMessage);
return false;
}
}
if (_executionPolicy == ExecutionPolicy.Unrestricted)
{
// Product binaries are always trusted
// This avoids signature and security zone checks
if (SecuritySupport.IsProductBinary(path))
return true;
// We need to give the "Remote File" warning
// if the file originated from the internet
if (!IsLocalFile(fi.FullName))
{
// Get the signature of the file.
if (String.IsNullOrEmpty(script.ScriptContents))
{
reasonMessage = StringUtil.Format(Authenticode.Reason_FileContentUnavailable, path);
reason = new UnauthorizedAccessException(reasonMessage);
return false;
}
Signature signature = GetSignatureWithEncodingRetry(path, script);
// The file is signed, with a publisher that
// we trust
if (signature.Status == SignatureStatus.Valid)
{
// The file is signed by a trusted publisher
if (IsTrustedPublisher(signature, path))
{
policyCheckPassed = true;
}
}
// We don't care about the signature. If you distrust them,
// or the signature does not exist, we prompt you only
// because it's remote.
if (!policyCheckPassed)
{
RunPromptDecision decision = RunPromptDecision.DoNotRun;
// Get their remote prompt answer, allowing them to
// enter nested prompts, if wanted.
do
{
decision = RemoteFilePrompt(path, host);
if (decision == RunPromptDecision.Suspend)
host.EnterNestedPrompt();
} while (decision == RunPromptDecision.Suspend);
switch (decision)
{
case RunPromptDecision.RunOnce:
policyCheckPassed = true;
break;
case RunPromptDecision.DoNotRun:
default:
policyCheckPassed = false;
reasonMessage = StringUtil.Format(Authenticode.Reason_DoNotRun, path);
reason = new UnauthorizedAccessException(reasonMessage);
break;
}
}
}
else
{
policyCheckPassed = true;
}
}
// Don't need to check the signature if the file is local
// and we're in "RemoteSigned" mode
else if ((IsLocalFile(fi.FullName)) &&
(_executionPolicy == ExecutionPolicy.RemoteSigned))
{
policyCheckPassed = true;
}
else if ((_executionPolicy == ExecutionPolicy.AllSigned) ||
(_executionPolicy == ExecutionPolicy.RemoteSigned))
{
// if policy requires signature verification,
// make it so.
// Get the signature of the file.
if (String.IsNullOrEmpty(script.ScriptContents))
{
reasonMessage = StringUtil.Format(Authenticode.Reason_FileContentUnavailable, path);
reason = new UnauthorizedAccessException(reasonMessage);
return false;
}
Signature signature = GetSignatureWithEncodingRetry(path, script);
// The file is signed.
if (signature.Status == SignatureStatus.Valid)
{
// The file is signed by a trusted publisher
if (IsTrustedPublisher(signature, path))
{
policyCheckPassed = true;
}
// The file is signed by an unknown publisher,
// So prompt.
else
{
policyCheckPassed = SetPolicyFromAuthenticodePrompt(path, host, ref reason, signature);
}
}
// The file is UnknownError, NotSigned, HashMismatch,
// NotTrusted, NotSupportedFileFormat
else
{
policyCheckPassed = false;
if (signature.Status == SignatureStatus.NotTrusted)
{
reason = new UnauthorizedAccessException(
StringUtil.Format(Authenticode.Reason_NotTrusted,
path,
signature.SignerCertificate.SubjectName.Name));
}
else
{
reason = new UnauthorizedAccessException(
StringUtil.Format(Authenticode.Reason_Unknown,
path,
signature.StatusMessage));
}
}
}
else // if(executionPolicy == ExecutionPolicy.Restricted)
{
// Deny everything
policyCheckPassed = false;
// But accept mshxml files from publishers that we
// trust, or files in the system protected directories
bool reasonSet = false;
if (String.Equals(fi.Extension, ".ps1xml", StringComparison.OrdinalIgnoreCase))
{
string[] trustedDirectories = new string[]
{ Platform.GetFolderPath(Environment.SpecialFolder.System),
Platform.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
};
foreach (string trustedDirectory in trustedDirectories)
{
if (fi.FullName.StartsWith(trustedDirectory, StringComparison.OrdinalIgnoreCase))
policyCheckPassed = true;
}
if (!policyCheckPassed)
{
// Get the signature of the file.
Signature signature = GetSignatureWithEncodingRetry(path, script);
// The file is signed by a trusted publisher
if (signature.Status == SignatureStatus.Valid)
{
if (IsTrustedPublisher(signature, path))
{
policyCheckPassed = true;
}
// The file is signed by an unknown publisher,
// So prompt.
else
{
policyCheckPassed = SetPolicyFromAuthenticodePrompt(path, host, ref reason, signature);
reasonSet = true;
}
}
}
}
if (!policyCheckPassed && !reasonSet)
{
reason = new UnauthorizedAccessException(
StringUtil.Format(Authenticode.Reason_RestrictedMode,
path));
}
}
return policyCheckPassed;
}
private bool SetPolicyFromAuthenticodePrompt(string path, PSHost host, ref Exception reason, Signature signature)
{
bool policyCheckPassed = false;
string reasonMessage;
RunPromptDecision decision = AuthenticodePrompt(path, signature, host);
switch (decision)
{
case RunPromptDecision.RunOnce:
policyCheckPassed = true; break;
case RunPromptDecision.AlwaysRun:
{
TrustPublisher(signature);
policyCheckPassed = true;
}; break;
case RunPromptDecision.DoNotRun:
policyCheckPassed = false;
reasonMessage = StringUtil.Format(Authenticode.Reason_DoNotRun, path);
reason = new UnauthorizedAccessException(reasonMessage);
break;
case RunPromptDecision.NeverRun:
{
UntrustPublisher(signature);
reasonMessage = StringUtil.Format(Authenticode.Reason_NeverRun, path);
reason = new UnauthorizedAccessException(reasonMessage);
policyCheckPassed = false;
}; break;
}
return policyCheckPassed;
}
private bool IsLocalFile(string filename)
{
#if UNIX
return true;
#else
SecurityZone zone = ClrFacade.GetFileSecurityZone(filename);
if (zone == SecurityZone.MyComputer ||
zone == SecurityZone.Intranet ||
zone == SecurityZone.Trusted)
{
return true;
}
return false;
#endif
}
// Checks that a publisher is trusted by the system or is one of
// the signed product binaries
private bool IsTrustedPublisher(Signature signature, string file)
{
// Get the thumbprint of the current signature
X509Certificate2 signerCertificate = signature.SignerCertificate;
string thumbprint = signerCertificate.Thumbprint;
// See if it matches any in the list of trusted publishers
X509Store trustedPublishers = new X509Store(StoreName.TrustedPublisher, StoreLocation.CurrentUser);
trustedPublishers.Open(OpenFlags.ReadOnly);
foreach (X509Certificate2 trustedCertificate in trustedPublishers.Certificates)
{
if (String.Equals(trustedCertificate.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase))
if (!IsUntrustedPublisher(signature, file)) return true;
}
return false;
}
private bool IsUntrustedPublisher(Signature signature, string file)
{
// Get the thumbprint of the current signature
X509Certificate2 signerCertificate = signature.SignerCertificate;
string thumbprint = signerCertificate.Thumbprint;
// See if it matches any in the list of trusted publishers
X509Store trustedPublishers = new X509Store(StoreName.Disallowed, StoreLocation.CurrentUser);
trustedPublishers.Open(OpenFlags.ReadOnly);
foreach (X509Certificate2 trustedCertificate in trustedPublishers.Certificates)
{
if (String.Equals(trustedCertificate.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
/// <summary>
/// Trust a publisher by adding it to the "Trusted Publishers" store
/// </summary>
/// <param name="signature"></param>
private void TrustPublisher(Signature signature)
{
// Get the certificate of the signer
X509Certificate2 signerCertificate = signature.SignerCertificate;
X509Store trustedPublishers = new X509Store(StoreName.TrustedPublisher, StoreLocation.CurrentUser);
try
{
// Add it to the list of trusted publishers
trustedPublishers.Open(OpenFlags.ReadWrite);
trustedPublishers.Add(signerCertificate);
}
finally
{
trustedPublishers.Close();
}
}
private void UntrustPublisher(Signature signature)
{
// Get the certificate of the signer
X509Certificate2 signerCertificate = signature.SignerCertificate;
X509Store untrustedPublishers = new X509Store(StoreName.Disallowed, StoreLocation.CurrentUser);
X509Store trustedPublishers = new X509Store(StoreName.TrustedPublisher, StoreLocation.CurrentUser);
// Remove it from the list of trusted publishers
try
{
// Remove the signer, if it's there
trustedPublishers.Open(OpenFlags.ReadWrite);
trustedPublishers.Remove(signerCertificate);
}
finally
{
trustedPublishers.Close();
}
try
{
// Add it to the list of untrusted publishers
untrustedPublishers.Open(OpenFlags.ReadWrite);
untrustedPublishers.Add(signerCertificate);
}
finally
{
untrustedPublishers.Close();
}
}
private Signature GetSignatureWithEncodingRetry(string path, ExternalScriptInfo script)
{
string verificationContents = System.Text.Encoding.Unicode.GetString(script.OriginalEncoding.GetPreamble()) + script.ScriptContents;
Signature signature = SignatureHelper.GetSignature(path, verificationContents);
// If the file was originally ASCII or UTF8, the SIP may have added the Unicode BOM
if ((signature.Status != SignatureStatus.Valid) && (script.OriginalEncoding != System.Text.Encoding.Unicode))
{
verificationContents = System.Text.Encoding.Unicode.GetString(System.Text.Encoding.Unicode.GetPreamble()) + script.ScriptContents;
Signature fallbackSignature = SignatureHelper.GetSignature(path, verificationContents);
if (fallbackSignature.Status == SignatureStatus.Valid)
signature = fallbackSignature;
}
return signature;
}
#endregion signing check
/// <summary>
/// Determines if should run the specified command. Please see the
/// class summary for an overview of the semantics enforced by this
/// authorization manager.
/// </summary>
/// <param name="commandInfo">
/// The command to be run.
/// </param>
/// <param name="origin">
/// The origin of the command.
/// </param>
/// <param name="host">
/// The PSHost executing the command.
/// </param>
/// <param name="reason">
/// If access is denied, this parameter provides a specialized
/// Exception as the reason.
/// </param>
/// <returns>
/// True if the command should be run. False otherwise.
/// </returns>
/// <exception cref="System.ArgumentException">
/// CommandInfo is invalid. This may occur if
/// commandInfo.Name is null or empty.
/// </exception>
/// <exception cref="System.ArgumentNullException">
/// CommandInfo is null.
/// </exception>
/// <exception cref="System.IO.FileNotFoundException">
/// The file specified by commandInfo.Path is not found.
/// </exception>
protected internal override bool ShouldRun(CommandInfo commandInfo,
CommandOrigin origin,
PSHost host,
out Exception reason)
{
Dbg.Diagnostics.Assert(commandInfo != null, "caller should validate the parameter");
bool allowRun = false;
reason = null;
Utils.CheckArgForNull(commandInfo, "commandInfo");
Utils.CheckArgForNullOrEmpty(commandInfo.Name, "commandInfo.Name");
switch (commandInfo.CommandType)
{
case CommandTypes.Cmdlet:
// Always allow cmdlets to run
allowRun = true;
break;
case CommandTypes.Alias:
//
// we do not care about verifying an alias as we will
// get subsequent call(s) for commands/scripts
// when the alias is expanded.
//
allowRun = true;
break;
case CommandTypes.Function:
case CommandTypes.Filter:
case CommandTypes.Configuration:
//
// we do not check functions/filters.
// we only perform script level check.
//
allowRun = true;
break;
case CommandTypes.Script:
//
// Allow scripts that are built into the
// runspace configuration to run.
//
allowRun = true;
break;
case CommandTypes.ExternalScript:
ExternalScriptInfo si = commandInfo as ExternalScriptInfo;
if (si == null)
{
reason = PSTraceSource.NewArgumentException("scriptInfo");
}
else
{
bool etwEnabled = ParserEventSource.Log.IsEnabled();
if (etwEnabled) ParserEventSource.Log.CheckSecurityStart(si.Path);
allowRun = CheckPolicy(si, host, out reason);
if (etwEnabled) ParserEventSource.Log.CheckSecurityStop(si.Path);
}
break;
case CommandTypes.Application:
//
// We do not check executables -- that is done by Windows
//
allowRun = true;
break;
}
return allowRun;
}
private RunPromptDecision AuthenticodePrompt(string path,
Signature signature,
PSHost host)
{
if ((host == null) || (host.UI == null))
{
return RunPromptDecision.DoNotRun;
}
RunPromptDecision decision = RunPromptDecision.DoNotRun;
if (signature == null)
{
return decision;
}
switch (signature.Status)
{
//
// we do not allow execution in any one of the
// following cases
//
case SignatureStatus.UnknownError:
case SignatureStatus.NotSigned:
case SignatureStatus.HashMismatch:
case SignatureStatus.NotSupportedFileFormat:
decision = RunPromptDecision.DoNotRun;
break;
case SignatureStatus.Valid:
Collection<ChoiceDescription> choices = GetAuthenticodePromptChoices();
string promptCaption =
Authenticode.AuthenticodePromptCaption;
string promptText;
if (signature.SignerCertificate == null)
{
promptText =
StringUtil.Format(Authenticode.AuthenticodePromptText_UnknownPublisher,
path);
}
else
{
promptText =
StringUtil.Format(Authenticode.AuthenticodePromptText,
path,
signature.SignerCertificate.SubjectName.Name
);
}
int userChoice =
host.UI.PromptForChoice(promptCaption,
promptText,
choices,
(int)RunPromptDecision.DoNotRun);
decision = (RunPromptDecision)userChoice;
break;
//
// if the publisher is not trusted, we prompt and
// ask the user if s/he wants to allow it to run
//
default:
decision = RunPromptDecision.DoNotRun;
break;
}
return decision;
}
private RunPromptDecision RemoteFilePrompt(string path, PSHost host)
{
if ((host == null) || (host.UI == null))
{
return RunPromptDecision.DoNotRun;
}
Collection<ChoiceDescription> choices = GetRemoteFilePromptChoices();
string promptCaption =
Authenticode.RemoteFilePromptCaption;
string promptText =
StringUtil.Format(Authenticode.RemoteFilePromptText,
path);
int userChoice = host.UI.PromptForChoice(promptCaption,
promptText,
choices,
0);
switch (userChoice)
{
case 0: return RunPromptDecision.DoNotRun;
case 1: return RunPromptDecision.RunOnce;
case 2: return RunPromptDecision.Suspend;
default: return RunPromptDecision.DoNotRun;
}
}
private Collection<ChoiceDescription> GetAuthenticodePromptChoices()
{
Collection<ChoiceDescription> choices = new Collection<ChoiceDescription>();
string neverRun = Authenticode.Choice_NeverRun;
string neverRunHelp = Authenticode.Choice_NeverRun_Help;
string doNotRun = Authenticode.Choice_DoNotRun;
string doNotRunHelp = Authenticode.Choice_DoNotRun_Help;
string runOnce = Authenticode.Choice_RunOnce;
string runOnceHelp = Authenticode.Choice_RunOnce_Help;
string alwaysRun = Authenticode.Choice_AlwaysRun;
string alwaysRunHelp = Authenticode.Choice_AlwaysRun_Help;
choices.Add(new ChoiceDescription(neverRun, neverRunHelp));
choices.Add(new ChoiceDescription(doNotRun, doNotRunHelp));
choices.Add(new ChoiceDescription(runOnce, runOnceHelp));
choices.Add(new ChoiceDescription(alwaysRun, alwaysRunHelp));
return choices;
}
private Collection<ChoiceDescription> GetRemoteFilePromptChoices()
{
Collection<ChoiceDescription> choices = new Collection<ChoiceDescription>();
string doNotRun = Authenticode.Choice_DoNotRun;
string doNotRunHelp = Authenticode.Choice_DoNotRun_Help;
string runOnce = Authenticode.Choice_RunOnce;
string runOnceHelp = Authenticode.Choice_RunOnce_Help;
string suspend = Authenticode.Choice_Suspend;
string suspendHelp = Authenticode.Choice_Suspend_Help;
choices.Add(new ChoiceDescription(doNotRun, doNotRunHelp));
choices.Add(new ChoiceDescription(runOnce, runOnceHelp));
choices.Add(new ChoiceDescription(suspend, suspendHelp));
return choices;
}
}
}