Powershell: Execution Policy RemoteSigned interpreted differently in pwsh7.1 / Unblock-File does not work

Created on 25 Oct 2020  Β·  34Comments  Β·  Source: PowerShell/PowerShell

Invoking Pester (v5.1.0-beta 2) on Pwsh 7.1.0-rc.2 leads to different behaviour regarding Execution Policy RemoteSigned and Unblock-File

Reading about_execution_policies RemoteSigned should allow files to run that have been unblocked, yet exeucting the same files (which are located on my local home server) yields two different results:
image

Steps to reproduce

Executing Invoke-Pester with files that are not digitally signed, not on a _this_ machine but also not downloaded (i.E. I have written these in the same environment with VSCode...

invoke-pester #errors
unblock-file \\Server\Share\File.ps1 #no output, i.E. OK
invoke-pester #still errors

I have had also tried a Get-ChildItem *.ps1 -Recurse | Unblock-File which did the same (nothing)

Expected behavior

Same environment, when my code base is on my local machine. Works.
Same environment in PowerShell 5.1. Works.

invoke-pester

Starting discovery in 4 files.
Discovery finished in 469ms.
Testing 'TeamsFunctions' in path '\\SERVERNAME\Share\Code\Personal\TeamsFunctions'
[ASSERT ] ERROR: You must call the Connect-SkypeOnline cmdlet before calling any other cmdlets.
[ASSERT ] INFO:  Connect-Me can be used to disconnect, then connect to SkypeOnline, AzureAD & MicrosoftTeams in one step!
[ASSERT ] ERROR: You must call the Connect-SkypeOnline cmdlet before calling any other cmdlets.
[ASSERT ] INFO:  Connect-Me can be used to disconnect, then connect to SkypeOnline, AzureAD & MicrosoftTeams in one step!
[+] Enable-TeamsUserForEnterpriseVoice.Tests.ps1 89ms (11ms|59ms)
[+] Show-FunctionStatus.Tests.ps1 89ms (2ms|68ms)
[+] Format-StringForUse.Tests.ps1 173ms (84ms|65ms)
Tests completed in 12.66s
Tests Passed: 779, Failed: 0, Skipped: 0 NotRun: 0

Actual behavior

invoke-pester

Starting discovery in 4 files.
System.Management.Automation.PSSecurityException: File \\SERVERNAME\Share\Code\Personal\TeamsFunctions\Private\Tests\Enable-TeamsUserForEnterpriseVoice.Tests.ps1 cannot be loaded. The file \\SERVERNAME\Share\Code\Personal\TeamsFunctions\Private\Tests\Enable-TeamsUserForEnterpriseVoice.Tests.ps1 is not digitally signed. You cannot run this script on the current system. For more information about running scripts and setting execution policy, see about_Execution_Policies at https://go.microsoft.com/fwlink/?LinkID=135170.
 ---> System.UnauthorizedAccessException: File \\SERVERNAME\Share\Code\Personal\TeamsFunctions\Private\Tests\Enable-TeamsUserForEnterpriseVoice.Tests.ps1 cannot be loaded. The file \\SERVERNAME\Share\Code\Personal\TeamsFunctions\Private\Tests\Enable-TeamsUserForEnterpriseVoice.Tests.ps1 is not digitally signed. You cannot run this script on the current system. For more information about running scripts and setting execution policy, see about_Execution_Policies at https://go.microsoft.com/fwlink/?LinkID=135170.
   --- End of inner exception stack trace ---
   at System.Management.Automation.ExceptionHandlingOps.CheckActionPreference(FunctionContext funcContext, Exception exception)
   at System.Management.Automation.Interpreter.ActionCallInstruction`2.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.Interpreter.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.LightLambda.RunVoid1[T0](T0 arg0)
   at System.Management.Automation.PSScriptCmdlet.RunClause(Action`1 clause, Object dollarUnderbar, Object inputToProcess)
   at System.Management.Automation.PSScriptCmdlet.DoEndProcessing()
   at System.Management.Automation.CommandProcessorBase.Complete()
at <ScriptBlock>, C:\Program Files\WindowsPowerShell\Modules\Pester\5.1.0\Pester.psm1: line 2899
at Invoke-File, C:\Program Files\WindowsPowerShell\Modules\Pester\5.1.0\Pester.psm1: line 2908
at Invoke-BlockContainer, C:\Program Files\WindowsPowerShell\Modules\Pester\5.1.0\Pester.psm1: line 2833
at Discover-Test, C:\Program Files\WindowsPowerShell\Modules\Pester\5.1.0\Pester.psm1: line 1407
at Invoke-Test, C:\Program Files\WindowsPowerShell\Modules\Pester\5.1.0\Pester.psm1: line 2352
at Invoke-Pester<End>, C:\Program Files\WindowsPowerShell\Modules\Pester\5.1.0\Pester.psm1: line 4753
at <ScriptBlock>, <No file>: line 1

Environment data

Name                           Value
----                           -----
PSVersion                      7.1.0-rc.2
PSEdition                      Core
GitCommitId                    7.1.0-rc.2
OS                             Microsoft Windows 10.0.19041
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
Issue-Question Resolution-Answered WG-Security

Most helpful comment

@DEberhardt

Unblock file works (actually I have not verified the change but prior of running the command, but as we found a bug elsewhere, should I test it still?)
effect of Unblock file can be verified (will add to my toolbox :))

Yes, it works, but if you want to play with it yourself, you can easily simulate blocked files with something like the following:

"[ZoneTransfer]`nZoneId=3" | Set-Content script.ps1 -Stream Zone.Identifier

Execution should then be refused / trigger a prompt with RemoteSigned / Unrestricted.

Calling Unblock-File on that file should implicitly make it safe, _unless_ it is called via a UNC path with . in the server name.

A successful Unblock-File call clears the Zone.Identifier stream, so that the querying the stream should then fail:

Get-Content: Could not open the alternate data stream 'Zone.Identifier' of the file '...\script.ps1'.
  • Servername with Fqdn (I.e. containing β€˜.’) should now be classed as Internet zone and should be flagged with the exception if the file is not unblocked first.

Yes, re considering them to be the unsafe Internet zone, but note that unblocking alone is _not_ enough in this case.
Only using a mapped drive or a "non-dotted" server name will allow you to run an unblocked file from that location.

Conversely, you _could_ use the Zone.Identifier stream to explicitly mark a file as _safe_, namely if you set it to 0, 1, or 2, which would then allow the file to run even when accessed with a UNC path based on a "dotted" server name, but I'm not sure that's a good approach.

Servernames without a dot should be identified as Intranet zone and work with RemoteSigned without needing to unblock

You _do_ need to unblock in this case, because the Zone.Identifier information always takes precedence (except in the unlikely event that it is set to -1 (NoZone)).

Note that a file gets only blocked if a download tool explicitly, by convention, adds the Zone.Identifier information, which mostly seems limited to _browsers_.

I've updated the summary above to reflect the latest understanding, let me know if something is still unclear.

All 34 comments

Also posted this on Pester, for awareness #1736

Is there a command to verify whether a file is currently blocked?
I have not found any and Get-Command -Noun File only yields 2 results. (Get-ChildItem also does not have IsBlockedor IsUnblockedas a parameter either...)

Was Block-File or Get-File or Test-File never thought of?

What's the result from the WinPS and pwsh sessions for Get-ExecutionPolicy -List?

image

To see if a file is blocked, run this:

get-item <path> -Stream Zone.Identifier

Or

get-item <path> -Stream * | % Stream

If the stream exists, then check the content of that stream:

PS> get-content <path> Stream Zone.Identifier
[ZoneTransfer]
ZoneId=3

If the ZoneId is 3 (or 4), then the file is blocked.

If you want to inspect an entire directory (tree) at once (for a single directory, you can omit the Get-ChildItem call and pass * to Get-Item):

Get-ChildItem -File -Recurse | Get-Item -ea Ignore -Stream Zone.Identifier | select Filename, @{ n='StreamContent'; e={ $_ | Get-Content } }

Thank you! This is helpful. However, Ran it, came back empty (nothing blocked?) and still fails with System.Management.Automation.PSSecurityException

Try starting PowerShell 7 like so from the Windows + R run dialog:

pwsh-preview -executionPolicy bypass

Then try Pester. Does it still fail the same way?

Another experiment to try - if your test files are not on a share (copy them to a local folder), does Pester still fail when run without the bypass used above?

With Bypass, it works normally.
Running it from a local source, also is successful.
The combination of RemoteSigned and local share results in the error (despite no files being blocked...)

Is it possible your network share is defined to be in the internet security zone?

It is by-design because now we can not use IE API https://github.com/PowerShell/PowerShell/issues/7458#issuecomment-613618200

Workaround is to set ByPass policy.

If I read this correctly, the IE zones will determine the trust PowerShell extends for RemoteSigned?
I would want to avoid Workarounds or Bypass. Integrated Security by design is good πŸ‘
Will evaluate the Zones, most likely I have not listed my server there (it is not a domain environment).
Reporting back in the next few days.
Cheers.

@DEberhardt PowerShell 7 can not use IE API. It uses simple heuristic. It is a security area and it is very sensitive area. It's incredible that something would be changed there without research by security experts.

it is not a domain environment

Since it is not a managed environment the best you can do is use ByPass policy.

ok. Thank you for that background info.
Follow-up question. Should Unblocking the files not do the trick?
or is the RemoteSigned really working differently in PS7?

Reading further in the linked issues, has the plan to cover this with the Settings file been progressed or is it still to-do?

Thanks,
David

Since the script is stored on a non-local path, powershell does treat that as remote, aye.

I do believe the behaviour for UNC paths was changed slightly fairly early on in 6.0 or 6.1 for pwsh, but hasn't changed since then to my knowledge.

Follow-up question. Should Unblocking the files not do the trick?
or is the RemoteSigned really working differently in PS7?

Yes, it works differently. The policy is a policy, the cmdlet cleans only an alternative data stream of the file and can not change a policy.

Associated issue #12336

Just to briefly mention the workaround in the interim: _if possible_, use the _short_ version of the target server's name (e.g, server1, rather than the FQDN, e.g. server1.example.org), because any . in it triggers the problem) in your UNC paths / drive mappings.

In my case, I am only using the Servername, not the FQDN

That is curious, @DEberhardt - on my non-domain-joined machine the presence of a . made the difference. Perhaps someone else can formulate a _generally_ effective workaround.

Thanks, @iSazonov. I conclude the following (I've conducted limited experiments - do tell me if I got something wrong; it still leaves the mystery of @DEberhardt seeing different behavior):

Workarounds for _intranet_ paths being treated as _internet_ paths in terms of security, causing refusal to run (policy RemoteSigned) / unwanted security prompts (Unrestricted):

  • Note: If a valid Zone.Identifier stream is present in the file targeted, its security zone information takes precedence; the subsequent bullet points at the same level therefore only apply to files whose Zone.Identifier stream is either invalid, set to -1, or missing altogether.

    • Zone.Identifier values 3 (Internet) and 4 (Untrusted), which correspond to the values of the SecurityZone enumeration, are what make a given file a _blocked_ one, but note that this stream information is only added if a download tool _explicitly, by convention_, adds it, which mostly seems limited to _browsers_ (which set the value to 3). Notably, neither Invoke-WebRequest -OutFile nor Invoke-RestMethod -OutFile (nor curl -o) add this stream, so you will get no warning when executing scripts downloaded this way - it is then only the form of the UNC path that matters, as described below.

    • Note that it is even possible to use the Zone.Identifier value to explicitly mark a file as _safe_, even if it is accessed via a UNC path that would otherwise be considered unsafe, as described below; that is, if the Zone.Identifier is set to 0, 1, or 2, the file is always considered safe.

  • When using UNC paths, if possible, refer to the server by its short name (no domain suffix; e.g., server1) instead of a FQDN (e..g, server1.example.com). Only then is the path considered to be in the Intranet zone, where files needn't be signed to be executable with either RemoteSigned or Unrestricted in effect.

    • Do not use IPv4 or IPv6 addresses or host names with periods (.); the only exception is if the IP address refers tot the _local machine_ (loopback address).
  • Alternatively, map a network drive and use a drive-letter-based path - any path accessed via a mapped network drive is considered to be in the Intranet zone.

  • Risky last resort: Set the execution policy to Bypass, if you control it.

Note that these workarounds aren't always feasible, notably in an organization where $PROFILE points to a UNC path based on a FQDN and setting the policy to Bypass isn't acceptable - see #7458.

A proposed enhancement, #12336, may bring the ability to whitelist servers as belonging to the intranet zone via powershell.config.json.

This issue has been marked as answered and has not had any activity for 1 day. It has been closed for housekeeping purposes.

This issue has not been answered AFAICT. There are some workarounds but based on what folks have said here, the workarounds should not be necessary.

@rkeithhill , but won't implementing the proposal in #12336, which you linked to, resolve the issue?

That is yet another workaround. Based on the algorithm, a UNC path containing <server-name-with-no-periods> should cause PowerShell to view the script as coming from the intranet zone. So a policy of RemoteSigned should allow an unsigned script to execute and that is not what the OP sees.

Hmm, now I wonder if PS is seeing the server name as Microsoft.PowerShell.Core\Filesystem::\\<server-none-fdq-name>?

That's an interesting lead - it would explain @DEberhardt's experience, judging by the screen shots.

Sorry - got myself confused: There must have been a regression between 7.0.3 and 7.1.0-rc.2:

In 7.0.3, both Set-Location \\<server-no-fdq-name>\share; .\script.ps1 and
\\<server-no-fdq-name>\share\script.ps1 work as expected.

In 7.1.0-rc.2, neither works.

Looks like a simple logic bug was introduced that accidentally reversed the logic:

The line that @iSazonov linked to:

return hostName.IndexOf('.') == -1 ? SecurityZone.Intranet : SecurityZone.Internet;

was incorrectly simplified to:

return hostName.Contains('.') ? SecurityZone.Intranet : SecurityZone.Internet;

whereas it should be the inverse:

return hostName.Contains('.') ? SecurityZone.Internet : SecurityZone.Intranet;

@rkeithhill A question in the issue was about Unblock-File and answer - user should use policy. Common problem we will continue to discuss in #12336.

Looks like a simple logic bug was introduced that accidentally reversed the logic

Thanks! It was typo in cleanup commit.

Woohoo, we found a bug and quashed it :)
Interesting. Haven’t seen the ternary operator in action yet
Thanks, this is very educational.

To summarise:

  • Unblock file works (actually I have not verified the change but prior of running the command, but as we found a bug elsewhere, should I test it still?)
  • effect of Unblock file can be verified (will add to my toolbox :))
  • Servername with Fqdn (I.e. containing β€˜.’) should now be classed as Internet zone and should be flagged with the exception if the file is not unblocked first.
  • Servernames without a dot should be identified as Intranet zone and work with RemoteSigned without needing to unblock (cheers)
  • Set-Location (what I used) should also work now.

Will this make it i to 7.1-rc3 ?

Thanks,
David

Best regards,
David

Apologies for the brief nature of this email as it has been sent from my phone.

@DEberhardt

Unblock file works (actually I have not verified the change but prior of running the command, but as we found a bug elsewhere, should I test it still?)
effect of Unblock file can be verified (will add to my toolbox :))

Yes, it works, but if you want to play with it yourself, you can easily simulate blocked files with something like the following:

"[ZoneTransfer]`nZoneId=3" | Set-Content script.ps1 -Stream Zone.Identifier

Execution should then be refused / trigger a prompt with RemoteSigned / Unrestricted.

Calling Unblock-File on that file should implicitly make it safe, _unless_ it is called via a UNC path with . in the server name.

A successful Unblock-File call clears the Zone.Identifier stream, so that the querying the stream should then fail:

Get-Content: Could not open the alternate data stream 'Zone.Identifier' of the file '...\script.ps1'.
  • Servername with Fqdn (I.e. containing β€˜.’) should now be classed as Internet zone and should be flagged with the exception if the file is not unblocked first.

Yes, re considering them to be the unsafe Internet zone, but note that unblocking alone is _not_ enough in this case.
Only using a mapped drive or a "non-dotted" server name will allow you to run an unblocked file from that location.

Conversely, you _could_ use the Zone.Identifier stream to explicitly mark a file as _safe_, namely if you set it to 0, 1, or 2, which would then allow the file to run even when accessed with a UNC path based on a "dotted" server name, but I'm not sure that's a good approach.

Servernames without a dot should be identified as Intranet zone and work with RemoteSigned without needing to unblock

You _do_ need to unblock in this case, because the Zone.Identifier information always takes precedence (except in the unlikely event that it is set to -1 (NoZone)).

Note that a file gets only blocked if a download tool explicitly, by convention, adds the Zone.Identifier information, which mostly seems limited to _browsers_.

I've updated the summary above to reflect the latest understanding, let me know if something is still unclear.

Thank you @mklement0 - I will play around with Security Identifiers, if only to understand them better πŸ˜„
The confusing bit for me was who decided that the Zone.Identifier would be there in the first place (browsers).
When reading up on signing of my Module, I learned to only sign the .psm1 file, not all linked .ps1 files (as it would impact performance), so I wondered why they'd still work if not signed. With your explanation I can now deduct that as they were installed through PowerShell with Install-Module they are local files and have not been set the identifier. Thank you for clearing that up.

This is sufficiently answered for me and the Fix is being progressed; I think we can put this issue to rest πŸ˜ƒ

Glad to hear it, @DEberhardt.

Re Install-Module: note that the enclosing module, PowerShellGet, has its own, independent security mechanism:

Whether you get a warning depends on whether the originating repository, as a whole, is considered _trusted_, as reflected in the InstallationPolicy column in the output from Get-PSRepository, and if and when a download is allowed, the files do _not_ have a Zone.Identifier stream, as you've observed.

Was this page helpful?
0 / 5 - 0 ratings