Secure Parameter Validation in PowerShell
Parameter validation in PowerShell is an extremely useful and easy way to alert a user as early as possible that incorrect input was provided to a function or cmdlet. The following attributes are available to you for performing explicit parameter validation (taken from MSDN):
ValidateCount
Specifies the minimum and maximum number of arguments that a parameter can accept. For more information about the syntax used to declare this attribute, see ValidateCount Attribute Declaration.
ValidateLength
Specifies the minimum and maximum number of characters in the parameter argument. For more information about the syntax used to declare this attribute, see ValidateLength Attribute Declaration.
ValidatePattern
Specifies a regular expression that validates the parameter argument. For more information about the syntax used to declare this attribute, see ValidatePattern Attribute Declaration.
ValidateRange
Specifies the minimum and maximum values of the parameter argument. For more information about the syntax used to declare this attribute, see ValidateRange Attribute Declaration.
ValidateSet
Specifies the valid values for the parameter argument. For more information about the syntax used to declare this attribute, see ValidateSet Attribute Declaration.
When validating parameters, it is important to make sure your validation attributes make sense though. For example, consider the following simple function that returns the status of a TCP connection:
function Test-TcpConnection
{
Param (
[String]$IPAddress,
[ValidateRange(1, 2147483647)]
[Int32]$Port
)
$TcpClient = New-Object System.Net.Sockets.TCPClient
try
{
$TcpClient.Connect($IPAddress, $Port)
}
catch [System.Net.Sockets.SocketException]
{ }
finally
{
$TcpClient.Connected
$TcpClient.Close()
}
}
Those comfortable with networking should recognize that the ValidateRange attribute doesn’t make sense since a port number is an unsigned, 16-bit value – i.e. 0-65535. You would be surprised to see how many built-in PowerShell cmdlets use nonsensical ValidateRange arguments. Naturally, it would make more sense to provide the following ValidateRange attribute:
[ValidateRange(1, 65535)]
Now, the validation attributes are nice and they certainly have their place but often times they are not necessary. By choosing proper data types as parameters to your functions, you will often get parameter validation for free (i.e. implicit validation). Consider the following improvement to the Test-TcpConnection function:
function Test-TcpConnection
{
Param (
[System.Net.IPAddress]$IPAddress,
[ValidateRange(1, [UInt16]::MaxValue)]
[UInt16]$Port
)
$TcpClient = New-Object System.Net.Sockets.TCPClient
try
{
$TcpClient.Connect($IPAddress, $Port)
}
catch [System.Net.Sockets.SocketException]
{ }
finally
{
$TcpClient.Connected
$TcpClient.Close()
}
}
By making the IPAddress parameter an IPAddress object, it will automatically parse the IP address provided. No complicated regular expressions required! For example, it will throw an error upon attempting to execute the following command:
Test-TcpConnection 0.0.0.300 80
It is also worth noting that the ValidateRange attribute is still necessary for the Port parameter because the “connect” method will not accept a port number of 0.
Starting in PowerShell v3, you also get automatic tab completion if you use the ValidateSet attribute or use an enum type as one of your parameters. Consider the following contrived example:
function Get-Poem
{
Param (
[ConsoleColor]
$Color1,
[ConsoleColor]
$Color2
)
Write-Host "Roses are $Color1, violets are $Color2, PowerShell is great and it is Blue."
}
Using the ConsoleColor enum as parameters provides two benefits:
- Only colors defined in the ConsoleColor enum will be accepted (in theory… more on this in a moment).
- You will get automatic tab completion when typing in the colors in PS v3 and v4- e.g. Get-Poem Re[TAB] Blu[TAB]
Now, there is a subtle characteristic of enums in .NET that can lead to undefined behavior: you can call the Parse method on an enum and have it return to you an undefined enum value. Consider the following example:
Get-Poem ([Enum]::Parse([ConsoleColor], 200)) ([Enum]::Parse([ConsoleColor], 42))
Roses are 200, violets are 42, PowerShell is great and it is Blue.
Obviously, I never intended for this behavior to occur. In fact, most people would think that they would be explicitly preventing this sort of thing from happening! If you were truly concerned about ensuring proper parameter validation, you would need to provide the following validation attribute for each parameter:
[ValidateScript({ [Enum]::IsDefined($_.GetType(), $_) })]
This ensures that the enum value you provided is an actual, defined value.
Now, to put things into perspective, I’ll provide an example of a built-in cmdlet introduced in PowerShell v3 that exhibits a lack of parameter validation that ultimately leads to undefined behavior – Resolve-DnsName.
Resolve-DnsName allows you to query individual DNS records of various types. You specify the record type through the “Type” parameter which accepts an enum of type [Microsoft.DnsClient.Commands.RecordType]. To view all of the defined record types, run the following:
[Microsoft.DnsClient.Commands.RecordType] | Get-Member -Static -MemberType Property
When I saw the list of record types, I noticed that a specific type was missing – AXFR (zone transfer). Penetration testers will often attempt to perform a zone transfer on a potentially misconfigured DNS server in order to get a listing of every DNS record in the zone specified. A successful unauthorized zone transfer is a potential treasure trove of actionable intelligence for an attacker. Naturally, I would have liked it if Resolve-DnsName would let you perform zone transfers (like nslookup.exe does) but unfortunately, there is no “AXFR” defined in the RecordType enum. Fortunately, we can use our new trick to force AXFR (0xFC) to be used:
Resolve-DnsName -Name zonetransfer.me -Type ([Enum]::Parse([Microsoft.DnsClient.Commands.RecordType], 0xfc)) -Server '209.62.64.46' -TcpOnly
If you ran this command, you would find that it only returns a single SOA entry. If you ran the command with Wireshark running, however, you would see that a zone transfer was actually performed. Unfortunately, Resolve-DnsName wasn’t designed to parse multiple records from a zone transfer but this is undefined behavior, nonetheless.
So, when choosing parameters you should follow this advice:
- Choose your parameter types wisely. Often choosing an appropriate type will provide implicit validation.
- Use parameter validation attributes as often as possible. Just make sure they make sense in the first place though.
- Beware of people like me that will try to exploit undefined behavior in your functions/cmdlets. 🙂