How to Handle Mandatory SecureString Parameters in PowerShell with Read-Host


4 views

When working with PowerShell scripts that require sensitive input like passwords, developers often face a dilemma: how to properly handle mandatory SecureString parameters while maintaining security and user experience. The common approach of using Read-Host directly in parameter declarations presents technical challenges.

PowerShell doesn't allow cmdlet execution within parameter default value assignments. This means code like:

[string]$password = Read-Host "Type password" -AsSecureString

will fail because default values must be constant expressions.

Here are three robust approaches to handle this scenario:

1. Parameter Validation Approach

Param (
    [Parameter(Mandatory=$true)]
    [string]$FileLocation,

    [Parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [Security.SecureString]$Password
)

if (-not $Password) {
    $Password = Read-Host "Enter password" -AsSecureString
}

2. Dynamic Parameter Approach

function Get-UserCredential {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$FileLocation,

        [Parameter(Mandatory=$false)]
        [Security.SecureString]$Password
    )

    process {
        if (-not $PSBoundParameters.ContainsKey('Password')) {
            $Password = Read-Host "Enter password" -AsSecureString
        }
        # Rest of your logic
    }
}

3. Advanced Function with Prompt

function Set-UserPassword {
    [CmdletBinding(DefaultParameterSetName='Prompt')]
    param(
        [Parameter(Mandatory=$true)]
        [string]$FileLocation,

        [Parameter(Mandatory=$true, ParameterSetName='Direct')]
        [Security.SecureString]$Password,

        [Parameter(ParameterSetName='Prompt')]
        [switch]$PromptForPassword
    )

    begin {
        if ($PSCmdlet.ParameterSetName -eq 'Prompt') {
            $Password = Read-Host "Enter password" -AsSecureString
        }
    }
}
  • Always use -AsSecureString for password input
  • Consider using ConvertTo-SecureString and ConvertFrom-SecureString for persistence
  • Avoid converting secure strings to plain text unnecessarily
  • Use the [Credential] attribute when working with PSCredential objects
function New-SecureUser {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Username,

        [Parameter(Mandatory=$false)]
        [Security.SecureString]$Password,

        [Parameter(Mandatory=$true)]
        [string]$OUPath
    )

    process {
        if (-not $PSBoundParameters.ContainsKey('Password')) {
            $Password = Read-Host "Enter password for $Username" -AsSecureString
            $Confirm = Read-Host "Confirm password" -AsSecureString
            
            $passPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
                [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password))
            $confirmPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
                [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Confirm))
            
            if ($passPlain -ne $confirmPlain) {
                throw "Passwords do not match"
            }
        }

        # User creation logic here
    }
}
  • Make SecureString parameters optional rather than mandatory
  • Provide clear prompts when interactive input is needed
  • Implement password confirmation for critical operations
  • Document your parameter requirements clearly in comment-based help
  • Consider using PSCredential objects for better integration with other cmdlets

When working with PowerShell scripts that require sensitive input like passwords, you might want to use Read-Host with the -AsSecureString parameter to securely capture user input. However, there's a common challenge when trying to use this directly in a mandatory parameter declaration.

Consider this typical parameter declaration:

Param (
    [Parameter(Mandatory=$True)]
    [string]$FileLocation,

    [Parameter(Mandatory=$True)]
    [string]$password = Read-Host "Type the password you would like to set all the users to" -assecurestring
)

The issue here is that you can't directly use Read-Host in the parameter default value assignment because:

  • Parameter attributes are evaluated at parse time, not runtime
  • Default values must be constant expressions
  • The secure string conversion complicates matters

Here are two effective approaches to handle this scenario:

Solution 1: Separate Parameter Validation

Param (
    [Parameter(Mandatory=$True)]
    [string]$FileLocation,

    [Parameter(Mandatory=$False)]
    [securestring]$Password
)

if (-not $Password) {
    $Password = Read-Host "Enter password" -AsSecureString
}

Solution 2: Using DynamicParam

For more advanced scenarios, you can use dynamic parameters:

function Get-CredentialInfo {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [string]$FileLocation
    )

    DynamicParam {
        $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        
        $attributes = New-Object System.Management.Automation.ParameterAttribute
        $attributes.Mandatory = $true
        
        $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $attributeCollection.Add($attributes)
        
        $dynParam = New-Object System.Management.Automation.RuntimeDefinedParameter(
            "Password",
            [securestring],
            $attributeCollection
        )
        
        $paramDictionary.Add("Password", $dynParam)
        return $paramDictionary
    }

    process {
        if (-not $PSBoundParameters.ContainsKey('Password')) {
            $PSBoundParameters['Password'] = Read-Host "Enter password" -AsSecureString
        }
        # Rest of your code
    }
}
  • Always use -AsSecureString for password input
  • Consider using Get-Credential for username/password combinations
  • Never store passwords in plain text
  • Use the [securestring] type instead of [string] for passwords

Here's a complete example showing how to properly handle password input:

Param (
    [Parameter(Mandatory=$True)]
    [string]$UserListFile,

    [Parameter(Mandatory=$False)]
    [securestring]$Password
)

# If password not provided, prompt for it
if (-not $Password) {
    $Password = Read-Host "Enter default password for new users" -AsSecureString
}

# Convert secure string to plain text (only when absolutely necessary)
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)
$PlainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)

# Process user creation
Get-Content $UserListFile | ForEach-Object {
    New-ADUser -Name $_ -AccountPassword $Password -Enabled $true
    Write-Host "Created user $_"
}

# Clear sensitive data from memory
$PlainPassword = $null
[GC]::Collect()