Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create a potocol handler for a Powershell script

How can I create a protocol handler for a powershell script and make the target powershell script receive command line arguments?

And what are the security concerns in doing so?

like image 782
JoakimE Avatar asked Oct 29 '25 01:10

JoakimE


1 Answers

To complement your helpful guide with sample code that automates creation of a custom protocol handler:

The following:

  • Creates a custom URI protocol custom: (rather than pwsh:, given that PowerShell is simply used to implement the protocol) to which an open-ended number of arguments can be passed.

  • Does so for the current user only (HKEY_CURRENT_USER\Software\Classes) by default; however, it's easy to tweak the code to implement the custom protocol for all users instead (HKEY_LOCAL_MACHINE\Software\Classes), though you'll need to run the code with elevation (as administrator) then.

    • A handler *.ps1 script is automatically created:

      • At $env:USERPROFILE\customUriHandler.ps1 in the current-user scenario.

      • At $env:ALLUSERPROFILE\customUriHandler.ps1 in the all-users scenario.

    • The handler script simply echoes the arguments passed to it, and it is invoked in a PowerShell script window that is kept open after script execution (-NoExit); tweak the PowerShell command as needed.

  • The protocol expects its arguments as if it were a shell command, i.e., as a space-separated list of arguments, with argument-individual "..." quoting, if necessary.

    • The sample command at the end uses Start-Process to invoke the following URI, which you could also submit from the Run dialog (WinKey-R), which passes arguments one, two & three, four:

      • URI: custom:one "two & three" four
      • Invocation via Start-Process: Start-Process 'custom:one "two & three" four'
    • Caveat: If you submit this URI via a web browser's address bar (note: doesn't seem to work with Microsoft Edge), it is URI-escaped, and a single one%20%22two%20&%20three%22%20four argument is passed instead, which would require custom parsing; similarly, submitting from File Explorer's address bar passes one%20two%20&%20three%20four, though note that the " chars. are - curiously - lost in the process.

# Determine the scope:
# Set to $false to install machine-wide (for all users)
# Note: Doing so then requires running with ELEVATION.
$currentUserOnly = $true

if (-not $currentUserOnly) {
  net session *>$null
  if ($LASTEXITCODE) { Throw "You must run this script as administrator (elevated)." }
}

$ErrorActionPreference = 'Stop'

# The name of the new protocol scheme
$schemeName = 'custom'

$pwshPathEscaped = (Get-Process -Id $PID).Path -replace '\\', '\\'

$handlerScript = ($env:ALLUSERSPROFILE, $env:USERPROFILE)[$currentUserOnly] + "\${schemeName}UriHandler.ps1"
$handlerScriptEscaped = $handlerScript -replace '\\', '\\'

# Create the protocol handler script.
@'

# Remove the protocol scheme name from the 1st argument.
$argArray = $args.Clone()
$argArray[0] = $argArray[0] -replace '^[^:]+:'
# If the 1st argument is now empty, remove it.
if ('' -eq $argArray[0]) { $argArray = $argArray[1..($argArray.Count-1)] }

"Received $($argArray.Count) argument(s)."

$i = 0
foreach ($arg in $argArray) {
  "#$((++$i)): [$arg]"
}

'@ > $handlerScript

# Construct a temp. *.reg file.
# Target the scope-appropriate root registrykey.
$rootKey = ('HKEY_LOCAL_MACHINE\Software\Classes', 'HKEY_CURRENT_USER\Software\Classes')[$currentUserOnly]
# Determine a temp. file path.
$tempFile = [IO.Path]::GetTempPath() + [IO.Path]::GetRandomFileName() + '.reg'
@"
Windows Registry Editor Version 5.00

[$rootKey\$schemeName]
@="URL:$schemeName"
"URL Protocol"=""

[$rootKey\$schemeName\DefaultIcon]
@="$pwshPathEscaped"

[$rootKey\$schemeName\shell]
@="open"

[$rootKey\$schemeName\shell\open\command]
; === Tweak the PowerShell command line here: ===
@="\"$pwshPathEscaped\" -ExecutionPolicy Bypass -NoProfile -NoExit -File \"$handlerScriptEscaped\" %1"

"@ > $tempFile

# Import the *.reg file into the registry.
& {
  $ErrorActionPreference = 'Continue'
  reg.exe import $tempFile 2>$null
  if ($LASTEXITCODE) { Throw "Importing with reg.exe failed: $tempFile"}
}

# Remove the temp. *.reg file.
Remove-Item -ErrorAction Ignore -LiteralPath $tempFile

# ---

# Sample invocation of the new protocol with 3 arguments:
$uri = "$schemeName`:one `"two & three`" four"
Write-Verbose -Verbose "Invoking the following URI: $uri"
Start-Process $uri
like image 115
mklement0 Avatar answered Oct 31 '25 15:10

mklement0