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:
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)
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
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
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
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 IsBlocked
or IsUnblocked
as 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
?
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.
.
); 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:
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.
Most helpful comment
@DEberhardt
Yes, it works, but if you want to play with it yourself, you can easily simulate blocked files with something like the following:
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 theZone.Identifier
stream, so that the querying the stream should then fail: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 to0
,1
, or2
, 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.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.