Skip to content

PowerShell Footguns

Things that will bite you coming from bash. Learn these before you break production.


Execution Policy Is Not a Security Boundary

powershell -ExecutionPolicy Bypass -File malware.ps1   # trivially bypassed
cat malware.ps1 | powershell -                          # also bypassed

It's a safety net against accidental script execution, not a security control. Don't cargo-cult it into hardening docs. Don't Set-ExecutionPolicy Unrestricted on production (that's chmod 777 energy). Use RemoteSigned or sign your scripts.


Object Pipeline vs Text Pipeline

Get-Process | grep nginx              # grep sees ToString() garbage
Get-Service | awk '{print $1}'        # awk gets mangled text

Get-Process | Where-Object { $_.Name -eq "nginx" }   # correct
Get-Service | Select-Object -ExpandProperty Name      # correct

The pipeline carries objects. Piping to native commands (grep, awk, sed) serializes to text and throws away structure. Stay in the object world.


Case Sensitivity Surprises

"Hello" -eq "hello"     # True   (case-INSENSITIVE by default!)
"Hello" -ceq "hello"    # False  (case-sensitive variant)

All comparison operators (-eq, -like, -match) are case-insensitive by default. Use -ceq, -clike, -cmatch when case matters. This catches everyone from bash/Python.


-WhatIf: Your Dry Run

Remove-Item C:\Logs\* -Recurse -WhatIf    # shows what WOULD be deleted, does nothing
Stop-Service -Name * -WhatIf               # preview before disaster
$WhatIfPreference = $true                  # make every cmdlet a dry run

Always -WhatIf first on bulk destructive operations. There is no --dry-run.


Double Quotes Interpolate Variables

"Items: $data.Count"        # WRONG: interpolates $data, appends ".Count" as text
"Items: $($data.Count)"     # RIGHT: subexpression forces property access
'No $interpolation here'    # single quotes are always literal

Invoke-Expression Is eval()

$userInput = "Get-Process; Remove-Item C:\ -Recurse -Force"
Invoke-Expression $userInput    # command injection

& $cmdlet                       # use the call operator instead

If you see Invoke-Expression with external data, it's a vulnerability.


PSRemoting Without HTTPS

Default WinRM uses HTTP (5985). Credentials are WinRM-encrypted but not TLS-wrapped. On untrusted networks, configure HTTPS (5986) with a certificate. And never set TrustedHosts = * on production.


Quick Reference: Bash Instinct vs PowerShell Reality

Your instinct What actually happens Do this instead
Pipe to grep Loses object properties Where-Object
== is case-sensitive -eq is case-insensitive -ceq
$var in single quotes expands Single quotes are literal Use double quotes
Exit code in $? $? is bool, not int $LASTEXITCODE
> file for redirect Clobbers encoding Out-File -Encoding UTF8