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 |