Tests/Pester.PSSecRules.Tests.ps1
|
BeforeAll { function RunRuleForCommand { param([String] $Command) $outputPath = Join-Path $env:TEMP ([IO.Path]::GetRandomFileName() + ".ps1") try { Set-Content -Path $outputPath -Value $Command Invoke-ScriptAnalyzer -Path $outputPath ` -CustomizedRulePath (Resolve-Path $PSScriptRoot\..\PSSecRules.psm1) ` -ExcludeRule PS* } finally { Remove-Item $outputPath -ErrorAction SilentlyContinue } } } Describe "Tests for PSSec.HardcodedCredential" { It "Should detect hardcoded password assignment" { $result = RunRuleForCommand { function Test-HardcodedCredential { $password = "sRbHG$a%" return $password.Length } } $result.RuleName | Should -Be "PSSec.HardcodedCredential" } It "Should detect hardcoded default credential parameter" { $result = RunRuleForCommand { function Test-HardcodedCredential { param([string]$ClientSecret = "my-fixed-secret") return $ClientSecret } } $result.RuleName | Should -Be "PSSec.HardcodedCredential" } It "Should detect hardcoded credential comparison" { $result = RunRuleForCommand { function Test-Login { param($userName, $pwdInput) if($userName -eq "admin" -and $pwdInput -eq "sRbHG$a%") { return $true } return $false } } $result.RuleName | Should -Be "PSSec.HardcodedCredential" } } Describe "Tests for PSSec.SqlInjection" { It "Should detect dynamic SQL with string interpolation" { $result = RunRuleForCommand { function Invoke-UnsafeSql { param($userInput) $query = "SELECT * FROM Users WHERE UserName = '$userInput'" Invoke-Sqlcmd -Query $query } } $result.RuleName | Should -Be "PSSec.SqlInjection" } It "Should detect CommandText assignment with concatenation" { $result = RunRuleForCommand { function Invoke-UnsafeSql { param($userInput) $cmd = New-Object psobject $cmd.CommandText = "SELECT * FROM Users WHERE UserName='" + $userInput + "'" return $cmd } } $result.RuleName | Should -Be "PSSec.SqlInjection" } } Describe "Tests for PSSec.PathTraversal" { It "Should detect tainted variable used as Get-Content path" { $result = RunRuleForCommand { function Get-UnsafeFileContent { param($pathInput) Get-Content -Path $pathInput } } $result.RuleName | Should -Be "PSSec.PathTraversal" } It "Should detect tainted variable in .NET file API" { $result = RunRuleForCommand { function Test-EngineSchema { param($pathInput) if($null -ne $pathInput) { $json = [System.IO.File]::ReadAllText($pathInput) return $json } } } $result.RuleName | Should -Be "PSSec.PathTraversal" } } Describe "Tests for PSSec.Xss" { It "Should detect tainted URL passed to Navigate" { $result = RunRuleForCommand { function Open-HtmlPage { param($urlQuery) $urlRequest = "http://mysite.com?" + $urlQuery $browser = New-Object psobject $browser.Navigate($urlRequest) } } $result.RuleName | Should -Be "PSSec.Xss" } It "Should detect tainted comment rendered via Write" { $result = RunRuleForCommand { function Show-Comment { param($userComment) $response = New-Object psobject $response.Write($userComment) } } $result.RuleName | Should -Be "PSSec.Xss" } It "Should allow encoded output before sink" { $result = RunRuleForCommand { function Open-HtmlPage { param($urlQuery) $encoded = [System.Net.WebUtility]::HtmlEncode($urlQuery) $browser = New-Object psobject $browser.Navigate($encoded) } } $result | Should -Be $null } } Describe "Tests for PSSec.InsecureDeserialization" { It "Should detect tainted json passed to JsonSerializer.Deserialize" { $result = RunRuleForCommand { function Test-EngineSchema { param($pathInput) if($null -ne $pathInput) { $json = [System.IO.File]::ReadAllText($pathInput) $schema = [System.Text.Json.JsonSerializer]::Deserialize($json) return $schema } } } $result.RuleName | Should -Be "PSSec.InsecureDeserialization" } It "Should detect tainted input in ConvertFrom-Json" { $result = RunRuleForCommand { function Read-Config { param($jsonInput) $obj = ConvertFrom-Json -InputObject $jsonInput return $obj } } $result.RuleName | Should -Be "PSSec.InsecureDeserialization" } It "Should allow constant json deserialization" { $result = RunRuleForCommand { function Read-Config { $json = '{"name":"safe"}' $obj = ConvertFrom-Json -InputObject $json return $obj } } $result | Should -Be $null } } Describe "Tests for PSSec.OldTlsProtocol" { It "Should detect old TLS protocol enum" { $result = RunRuleForCommand { function Invoke-ClientAuth { $protocol = [System.Security.Authentication.SslProtocols]::Tls return $protocol } } $result.RuleName | Should -Be "PSSec.OldTlsProtocol" } It "Should allow TLS 1.2" { $result = RunRuleForCommand { function Invoke-ClientAuth { $protocol = [System.Security.Authentication.SslProtocols]::Tls12 return $protocol } } $result | Should -Be $null } } Describe "Tests for PSSec.OutdatedCrypto" { It "Should detect SHA1 provider" { $result = RunRuleForCommand { function Get-HashValue { param($text) $enc = [System.Text.Encoding]::UTF8 $buffer = $enc.GetBytes($text) $sha1 = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider return $sha1.ComputeHash($buffer) } } $result.RuleName | Should -Be "PSSec.OutdatedCrypto" } It "Should allow SHA256 provider" { $result = RunRuleForCommand { function Get-HashValue { param($text) $enc = [System.Text.Encoding]::UTF8 $buffer = $enc.GetBytes($text) $sha256 = New-Object System.Security.Cryptography.SHA256CryptoServiceProvider return $sha256.ComputeHash($buffer) } } $result | Should -Be $null } } Describe "Tests for PSSec.Xxe" { It "Should detect insecure XmlDocument settings with tainted input" { $result = RunRuleForCommand { function Read-XmlUnsafe { param($xmlPathInput) $doc = New-Object System.Xml.XmlDocument $doc.XmlResolver = New-Object System.Xml.XmlUrlResolver $doc.Load($xmlPathInput) return $doc.InnerText } } $result.RuleName | Should -Be "PSSec.Xxe" } It "Should detect insecure XmlReaderSettings with DTD parse" { $result = RunRuleForCommand { function Read-XmlUnsafe { param($xmlPathInput) $settings = New-Object System.Xml.XmlReaderSettings $settings.XmlResolver = New-Object System.Xml.XmlUrlResolver $settings.DtdProcessing = [System.Xml.DtdProcessing]::Parse $reader = [System.Xml.XmlReader]::Create($xmlPathInput, $settings) return $reader } } $result.RuleName | Should -Be "PSSec.Xxe" } It "Should allow secure XmlReaderSettings" { $result = RunRuleForCommand { function Read-XmlSafe { param($xmlPathInput) $settings = New-Object System.Xml.XmlReaderSettings $settings.XmlResolver = $null $settings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit $reader = [System.Xml.XmlReader]::Create($xmlPathInput, $settings) return $reader } } $result | Should -Be $null } } Describe "Tests for PSSec.Xee" { It "Should detect XML entity expansion risk with unbounded settings" { $result = RunRuleForCommand { function Read-XmlUnsafe { param($xmlInput) $settings = New-Object System.Xml.XmlReaderSettings $settings.DtdProcessing = [System.Xml.DtdProcessing]::Parse $settings.MaxCharactersFromEntities = 0 $reader = [System.Xml.XmlReader]::Create($xmlInput, $settings) return $reader } } $result.RuleName | Should -Be "PSSec.Xee" } It "Should allow bounded and non-parse settings" { $result = RunRuleForCommand { function Read-XmlSafe { param($xmlInput) $settings = New-Object System.Xml.XmlReaderSettings $settings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit $settings.MaxCharactersFromEntities = 1024 $reader = [System.Xml.XmlReader]::Create($xmlInput, $settings) return $reader } } $result | Should -Be $null } } Describe "Tests for PSSec.SessionTimeout" { It "Should detect negative timeout assignment" { $result = RunRuleForCommand { function Set-Session { $session = [PSCustomObject]@{} $session.Timeout = -1 return $session } } $result.RuleName | Should -Be "PSSec.SessionTimeout" } It "Should detect excessive timeout assignment" { $result = RunRuleForCommand { function Set-Session { $session = [PSCustomObject]@{} $session.Timeout = 120 return $session } } $result.RuleName | Should -Be "PSSec.SessionTimeout" } It "Should allow normal timeout assignment" { $result = RunRuleForCommand { function Set-Session { $session = [PSCustomObject]@{} $session.Timeout = 30 return $session } } $result | Should -Be $null } } Describe "Tests for PSSec.Ssrf" { It "Should detect tainted URL in Invoke-WebRequest" { $result = RunRuleForCommand { function Invoke-Remote { param($url) Invoke-WebRequest -Uri $url } } $result.RuleName | Should -Be "PSSec.Ssrf" } It "Should detect tainted endpoint in Invoke-RestMethod" { $result = RunRuleForCommand { function Invoke-Remote { param($endpoint) $target = "http://$endpoint/api" Invoke-RestMethod -Uri $target } } $result.RuleName | Should -Be "PSSec.Ssrf" } It "Should allow constant URL request" { $result = RunRuleForCommand { function Invoke-Remote { $uri = "https://api.example.com/status" Invoke-RestMethod -Uri $uri } } $result | Should -Be $null } } Describe "Tests for PSSec.LogInjection" { It "Should detect tainted message in Write-EventLog" { $result = RunRuleForCommand { function Write-AppEvent { param($message) Write-EventLog -LogName Application -Source Demo -EntryType Information -EventId 1 -Message $message } } $result.RuleName | Should -Be "PSSec.LogInjection" } It "Should detect tainted message in Add-Content" { $result = RunRuleForCommand { function Write-AppLog { param($userInput) $line = "user=$userInput" Add-Content -Path .\app.log -Value $line } } $result.RuleName | Should -Be "PSSec.LogInjection" } It "Should allow escaped message before logging" { $result = RunRuleForCommand { function Write-AppLog { param($message) $safe = [System.Security.SecurityElement]::Escape($message) Add-Content -Path .\app.log -Value $safe } } $result | Should -Be $null } } Describe "Tests for PSSec.LdapInjection" { It "Should detect tainted LDAP filter assignment" { $result = RunRuleForCommand { function Search-LdapUnsafe { param($userName, $password) $filter = "(&(userId=$userName)(UserPassword=$password))" $searcher = New-Object psobject $searcher.Filter = $filter } } $result.RuleName | Should -Be "PSSec.LdapInjection" } It "Should detect tainted LDAPFilter command parameter" { $result = RunRuleForCommand { function Search-LdapUnsafe2 { param($employeeName) $ldap = "(&(objectClass=user)(employeename=$employeeName))" Get-ADUser -LDAPFilter $ldap } } $result.RuleName | Should -Be "PSSec.LdapInjection" } It "Should allow escaped LDAP values" { $result = RunRuleForCommand { function Search-LdapSafe { param($userName, $password) $safeUser = [Microsoft.Security.Application.Encoder]::LdapFilterEncode($userName) $safePwd = [Microsoft.Security.Application.Encoder]::LdapFilterEncode($password) $filter = "(&(userId=$safeUser)(UserPassword=$safePwd))" $searcher = New-Object psobject $searcher.Filter = $filter } } $result | Should -Be $null } } Describe "Tests for PSSec.SensitiveErrorExposure" { It "Should detect stack trace exposure in catch" { $result = RunRuleForCommand { function Show-ErrorUnsafe { param($value) try { [int]::Parse($value) | Out-Null } catch { Write-Output $_.ScriptStackTrace } } } $result.RuleName | Should -Be "PSSec.SensitiveErrorExposure" } It "Should detect raw exception output in catch" { $result = RunRuleForCommand { function Show-ErrorUnsafe2 { param($value) try { [int]::Parse($value) | Out-Null } catch { Write-Host $_ } } } $result.RuleName | Should -Be "PSSec.SensitiveErrorExposure" } It "Should allow generic error message" { $result = RunRuleForCommand { function Show-ErrorSafe { param($value) try { [int]::Parse($value) | Out-Null } catch { Write-Output "An error has occurred." } } } $result | Should -Be $null } } Describe "Tests for PSSec.XPathInjection" { It "Should detect tainted XPath expression in Evaluate" { $result = RunRuleForCommand { function Query-XmlUnsafe { param($username, $passwordHash) $query = "//users/user[username/text()='$username' and passwordHash/text()='$passwordHash']/data/text()" $navigator = New-Object psobject $navigator.Evaluate($query) } } $result.RuleName | Should -Be "PSSec.XPathInjection" } It "Should detect tainted XPath in Select-Xml" { $result = RunRuleForCommand { function Query-XmlUnsafe2 { param($xpathExpr) Select-Xml -Path .\users.xml -XPath $xpathExpr } } $result.RuleName | Should -Be "PSSec.XPathInjection" } It "Should allow escaped XPath values" { $result = RunRuleForCommand { function Query-XmlSafe { param($username, $passwordHash) $safeUser = [System.Security.SecurityElement]::Escape($username) $safeHash = [System.Security.SecurityElement]::Escape($passwordHash) $query = "//users/user[username/text()='$safeUser' and passwordHash/text()='$safeHash']/data/text()" $navigator = New-Object psobject $navigator.Evaluate($query) } } $result | Should -Be $null } } Describe "Tests for PSSec.OpenRedirect" { It "Should detect tainted URL in Redirect" { $result = RunRuleForCommand { function Redirect-Unsafe { param($redirectUrl) $response = New-Object psobject $response.Redirect($redirectUrl) } } $result.RuleName | Should -Be "PSSec.OpenRedirect" } It "Should detect tainted URL in RedirectPermanent" { $result = RunRuleForCommand { function Redirect-Unsafe2 { param($next) $url = $next $response = New-Object psobject $response.RedirectPermanent($url) } } $result.RuleName | Should -Be "PSSec.OpenRedirect" } It "Should allow IsLocalUrl validated redirect" { $result = RunRuleForCommand { function Redirect-Safe { param($redirectUrl) if([Microsoft.AspNet.Membership.OpenAuth.OpenAuth]::IsLocalUrl($redirectUrl)) { $response = New-Object psobject $response.Redirect($redirectUrl) } } } $result | Should -Be $null } } Describe "Tests for PSSec.TaintedConfig" { It "Should detect tainted config in connection string" { $result = RunRuleForCommand { function Set-DbConfigUnsafe { param($catalog) $dbConnection = New-Object psobject $dbConnection.ConnectionString = "Data Source=.;Initial Catalog=$catalog;User ID=sa;Password=pass;" } } @($result.RuleName) | Should -Contain "PSSec.TaintedConfig" } It "Should detect tainted config command argument" { $result = RunRuleForCommand { function Set-AppConfigUnsafe { param($tenant) Set-ItemProperty -Path HKLM:\Software\Demo -Name Config -Configuration $tenant } } @($result.RuleName) | Should -Contain "PSSec.TaintedConfig" } It "Should allow allowlisted configuration value" { $result = RunRuleForCommand { function Set-DbConfigSafe { param($catalog, [System.Collections.Generic.HashSet[string]]$validCatalogNames) if(-not $validCatalogNames.Contains($catalog)) { return } $dbConnection = New-Object psobject $dbConnection.ConnectionString = "Data Source=.;Initial Catalog=$catalog;User ID=sa;Password=pass;" } } @($result).Count | Should -Be 0 } } Describe "Tests for PSSec.VulnerablePackage" { It "Should detect vulnerable Google.Protobuf version" { $result = RunRuleForCommand { $pkg = "Google.Protobuf 3.11.4" Write-Output $pkg } @($result.RuleName) | Should -Contain "PSSec.VulnerablePackage" } It "Should detect vulnerable SSH.NET version" { $result = RunRuleForCommand { $pkg = "SSH.NET 2016.1.0" Write-Output $pkg } @($result.RuleName) | Should -Contain "PSSec.VulnerablePackage" } It "Should allow non-vulnerable package version" { $result = RunRuleForCommand { $pkg = "Google.Protobuf 3.21.12" Write-Output $pkg } @($result).Count | Should -Be 0 } } Describe "Tests for PSSec.ReDoS" { It "Should detect unsafe regex with tainted input" { $result = RunRuleForCommand { function Test-ReDoSUnsafe { param($name) $pattern = '(x+)+y' [regex]::IsMatch($name, $pattern) } } @($result.RuleName) | Should -Contain "PSSec.ReDoS" } It "Should detect inline unsafe regex pattern" { $result = RunRuleForCommand { function Test-ReDoSUnsafe2 { param($line) [regex]::Match($line, '^(-?\d+)*$').Success } } @($result.RuleName) | Should -Contain "PSSec.ReDoS" } It "Should allow safe regex pattern" { $result = RunRuleForCommand { function Test-ReDoSSafe { param($line) [regex]::IsMatch($line, '^(\d{2}-\d{2}-\d{4})$') } } @($result).Count | Should -Be 0 } } Describe "Tests for PSSec.NoSqlInjection" { It "Should detect tainted NoSQL filter string" { $result = RunRuleForCommand { function Find-UserUnsafe { param($login, $password) $filter = '{"$where":"function(){return this.login==''' + $login + ''' && this.password==''' + $password + ''';}"}' $collection = New-Object psobject $collection.Find($filter) } } @($result.RuleName) | Should -Contain "PSSec.NoSqlInjection" } It "Should detect tainted NoSQL command filter" { $result = RunRuleForCommand { function Delete-Unsafe { param($count) $query = '{"$where":"function(){return this.count == ' + $count + ';}"}' Remove-MongoItem -Filter $query } } @($result.RuleName) | Should -Contain "PSSec.NoSqlInjection" } It "Should allow typed NoSQL filter object" { $result = RunRuleForCommand { function Find-UserSafe { param($login, $password) $filter = @{ login = $login; password = $password } $collection = New-Object psobject $collection.Find($filter) } } @($result).Count | Should -Be 0 } } Describe "Tests for PSSec.ZipSlip" { It "Should detect tainted entry name used in ExtractToFile path" { $result = RunRuleForCommand { function Expand-ZipUnsafe { param($destinationDirectory, $entryName) $extractPath = [System.IO.Path]::Combine($destinationDirectory, $entryName) $entry = New-Object psobject $entry.ExtractToFile($extractPath, $true) } } @($result.RuleName) | Should -Contain "PSSec.ZipSlip" } It "Should allow validated extraction full path" { $result = RunRuleForCommand { function Expand-ZipSafe { param($destinationDirectory, $entryName) $extractPath = [System.IO.Path]::Combine($destinationDirectory, $entryName) $extractFullPath = [System.IO.Path]::GetFullPath($extractPath) $destinationDirectoryFullPath = [System.IO.Path]::GetFullPath($destinationDirectory) if(-not $extractFullPath.StartsWith($destinationDirectoryFullPath)) { throw "Zip Slip" } $entry = New-Object psobject $entry.ExtractToFile($extractFullPath, $true) } } @($result).Count | Should -Be 0 } } Describe "Tests for PSSec.InvisibleCharacter" { It "Should detect zero-width space in code" { $result = RunRuleForCommand "`$value = 'ab`u{200B}cd'" @($result.RuleName) | Should -Contain "PSSec.InvisibleCharacter" } It "Should allow normal visible text" { $result = RunRuleForCommand { $value = 'abcd' Write-Output $value } @($result).Count | Should -Be 0 } } Describe "Tests for PSSec.CookieInjection" { It "Should detect tainted value used for HttpCookie" { $result = RunRuleForCommand { function Set-CookieUnsafe { param($userRole) $cookie = New-Object System.Web.HttpCookie 'role', $userRole return $cookie } } @($result.RuleName) | Should -Contain "PSSec.CookieInjection" } It "Should allow validated cookie value" { $result = RunRuleForCommand { function Set-CookieSafe { param($cultureValue) if(-not [regex]::IsMatch($cultureValue, '^[a-zA-Z0-9_-]{2,20}$')) { return } $cookie = New-Object System.Web.HttpCookie 'culture', $cultureValue return $cookie } } @($result).Count | Should -Be 0 } } Describe "Tests for PSSec.ExternallyControlledFormatString" { It "Should detect tainted format string in string.Format" { $result = RunRuleForCommand { function Apply-UnsafeFormat { param($format, $name) [string]::Format($format, $name) } } @($result.RuleName) | Should -Contain "PSSec.ExternallyControlledFormatString" } It "Should allow constant format string" { $result = RunRuleForCommand { function Apply-SafeFormat { param($name) $format = "Hello {0}" [string]::Format($format, $name) } } @($result).Count | Should -Be 0 } } |