Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass argument from Powershell to Batch script

I need to pass a password with special characters from powershell script automation.ps1 to batch script batch_script.bat which pipes it to main.py. Piping from batch_script.bat to main.py works fine, that is authentication succeeds. However, when I run the entire procedure described above, authentication fails, but echoing the password shows the correct password string.

My guess is that there are issues with special characters. What is a safe way to pass these strings?

Background

I want to automate the daily download from some external source via a Python script main.py. This process requires a password. So I wrote a batch_script.bat which pipes the password to the Python script when prompted for it. However, I don't want to store the password as plain text in the batch script, so I encrypted the password and wrote another layer automation.ps1 which decrypts the password and passes it as plain text to batch_script.bat.

automation.ps1

# get password
$securePassword = Get-Content "<file_path>" | ConvertTo-SecureString
$credentials = New-Object System.Management.Automation.PsCredential("<user_name>",$securePassword)
$unsecurePassword = ($credentials).GetNetworkCredential().Password

# call script
$script = "<path>\batch_script.bat"
$args = @("`"<user name>`"","`"$unsecurePassword`"")
start-process $script $args

batch_script.bat

(I am aware that in this example I discard the passed username, just wanted to preserve the fact that I pass multiple arguments in case there is any relevance to it)

@echo off
SET username=%~1
SET password=%~2
echo(%password%|python main.py
like image 639
Jhonny Avatar asked May 21 '20 21:05

Jhonny


People also ask

How do I pass arguments to a PowerShell script?

The PowerShell script is a collection of commands saved in a .ps1 extension file. PowerShell executes those commands in sequence. This tutorial will introduce different methods to pass arguments to a PowerShell script. We can define arguments using the param statement. It also allows us to use the default values.

Is it possible to run a batch file as PowerShell?

It's usually simpler and easier to run the batch file as administrator/elevated, and Powershell will run with the same access rights as the batch file. Was this post helpful?

How to add $a as a parameter to a PowerShell script?

In your PowerShell script, add $a as a parameter to the batch file: In you batch file reference the parameter as %1. blat -s "Daily Report %1" -i "Our Team" -to [email protected],[email protected] -body "Please see attached." -priority 1 -attach D:\Script\Daily_Report.xlsx

What is a PowerShell script?

The PowerShell script is a collection of commands saved in a .ps1 extension file. PowerShell executes those commands in sequence. This tutorial will introduce different methods to pass arguments to a PowerShell script.


3 Answers

With following, all special characters should be handled very well. If any character required to be escaped, check this

$pass could be any string but check for special characters of powershell

$pass="%^&<>|'\``,;=(\)![]\/";
# Wait till it ends with -Wait when using -NoNewWindow.
# It may be comprehensible to use `" instead of "" to denote we are enclosing string in quotes.
(thanks @mklement0 for elaboration).
start-process -Wait -NoNewWindow .\script.cmd "`"$pass`""
script.cmd
    setlocal

    rem Remove Double quotes
    set "arg=%~1"

    rem Test result with base64 encoding
    echo|set/p="%arg%"|openssl base64
    rem echo is used with set/p to prevent trailing new line.
    echo|set/p="%arg%"|python main.py

    rem Test with following, argument is in double quotes
    rem script "%^&<>|'\`,;=(\)![]\/"
    rem Expected result
    rem %^&<>|'\`,;=(\)![]\/



like image 188
subcoder Avatar answered Oct 04 '22 10:10

subcoder


tl;dr:

  • Unless you specifically need the batch file to run in a new window, avoid Start-Process (whose built-in aliases are start and saps), and invoke the batch file directly.

  • To avoid problems with special characters in $unsecurePassword, do not pass it as an argument, pass it via stdin (the standard input stream), which your batch file will pass through to your python script:

automation.ps1:

# ...
$script = "<path>\batch_script.bat"
# Pass the password via *stdin*
$unsecurePassword | & $script 'userName'

Note: It is the $OutputEncoding preference variable that controls what character encoding PowerShell uses for sending text to an external program's stdin. In Windows PowerShell, that variable defaults to ASCII(!) encoding, meaning that any characters outside the 7-bit ASCII-range of Unicode characters, such as accented characters, are unsupported (they turn to literal ?); fortunately, PowerShell [Core] v6+ now defaults to UTF-8. Assign the required encoding to $OutputEncoding as needed.

batch_script.bat:

@echo off

SET username=%~1

REM The batch file will pass its own stdin input through to Python.
python main.py

Read on for background information.


Invoking a batch file from PowerShell:

Unless you truly need to launch a batch file in a new window, the best approach is to invoke it directly from PowerShell; that way, it runs:

  • in the same console window, synchronously.

  • with its output streams connected to PowerShell's (which allows you to capture or redirect the output).

Because your batch-file path is stored in a variable, direct invocation requires use of &, the call operator:

# Note: The " chars. around $unsecurePassword are only needed if the variable
#       value contains cmd.exe metacharacters - see next section.
& $script 'userA' `"$unsecurePassword`"

Start-Process is usually the wrong tool for invoking console applications, batch files, and other console-based scripts; see this answer for more information.

If you do need the batch file to run in a new window (which is only an option on Windows), use Start-Process as follows (the command will execute asynchronously, unless you also pass -Wait):

# The string with the arguments to pass is implicitly bound 
# to the -ArgumentList parameter. Use only " for embedded quoting.
Start-Process $script "userA `"$unsecurePassword`""

Note: While the (implied) -ArgumentList (-Args) parameter is array-valued ([string[]]) and passing the arguments individually is arguably the cleaner approach, this generally does not work properly, due to a longstanding bug that probably won't get fixed; for instance,
Start-Process foo.exe -Args 'one', 'two (2)' passes 3 arguments rather than 2; that is, it passes single string 'two (2)' as two arguments - see this GitHub issue.
Therefore, it is ultimately simpler and more predictable to pass a single argument with embedded quoting to -ArgumentList, but be sure to use only " (not ') for embedded quoting:
Start-Process foo.exe -Args "one `"two (2)`""


Passing arguments robustly to cmd.exe / batch files:

Note:

  • The limitations of cmd.exe (the legacy command processor that interprets batch files) prevent fully robust solutions; notably, you cannot prevent the interpretation of tokens such as %foo% as environment-variable references when you call a batch file from PowerShell (at least not without altering the argument to %foo^%, which will retain the ^).

  • In your specific case, since you're trying to echo an argument unquoted, embedded double quotes (") in such an argument - which need to be escaped as "" - aren't properly supported: they are passed through as "".

Passing an unquoted argument to cmd.exe / a batch file breaks, if that argument contains one of cmd.exe's metacharacters, i.e., characters with special syntactic meaning; in this context, they are: & | < > ^ "

The solution is to enclose the argument in double quotes ("..."), with the added need to double " chars. that are embedded (a part of the value).

PowerShell, after performing its own parsing of the command line (notably evaluating variable references and expressions), constructs the command line that is ultimately used to invoke the external target program, behind the scenes.

However, it only automatically double-quotes an argument if it contains spaces, not if it only contains cmd.exe metacharacters; e.g., a variable with verbatim string content two (2) is passed double-quoted - $val = 'two 2'; .\foo.bat $val results in command line .\foo.bat "two 2" - whereas string content a&b is not - $val = 'a&b'.\foo.bat $val results in .\foo.bat a&b - which breaks.

The solution - as shown in your question - is to enclose the variable reference in literal, embedded " characters, because such a "pre-quoted" value instructs PowerShell to pass the value as-is:
$val = 'a&b'; .\foo.bat `"$val`" results in .\foo.bat "a&b"

Note: .\foo.bat "`"$val`"" has the same effect; I'm taking advantage of the fact that PowerShell in argument (parsing) mode (generally) implicitly treats arguments as if they were double-quoted; in expression (parsing) mode, such as in the array-construction statement in the question (@(..., ...)), you do need the "`"$val`"" form.


The problem with your specific batch file:

A properly "..."-enclosed argument (with any embedded " chars. escaped as "") is properly seen as a parameter (e.g., %1) inside a batch file.

However, it is seen with the enclosing double quotes and with any doubled embedded " chars.

If you were to pass this parameter to the target program (python in this case) as an argument, everything would work as expected.

However, since you're passing the value via stdin using echo, you need to strip the enclosing double quotes so that they're not passed as part of the value, which is what your batch file attempts (e.g., %~2)

However, passing the stripped value causes the echo command to break.

There is no good solution to this problem with echo, short of performing cumbersome explicit ^-escaping (^ being cmd.exe's escape character):

$escapedUnsecurePassword = $unsecurePassword -replace '[&|<>^]' -replace '"', '""'
& $script 'userA' `"$escapedUnsecurePassword`"

That alone still isn't enough, however - your batch_script.bat file needs a modification too: Because the assignment itself in your SET password=%~2 command isn't protected with double quotes, it breaks with values that contain metacharacters; somewhat paradoxically, you must use the form SET "password=%~2" in order to safely strip the embedded enclosing " chars.:

@echo off

REM Strip the enclosing "..." from the arguments (%~<n>)
REM !! The *assignment itself* must be in "..."  so that
REM !! it does not break if the value has cmd.exe metacharacters.
set "username=%~1"
set "password=%~2"

echo(%password%|python main.py

Note that that will work as intended for all metacharacters except the - of necessity doubled - embedded ", which are passed through as "".


However, there is a workaround for echoing a string with metacharacters unquoted, as also demonstrated in subcoder's helpful answer:

If you define batch_script.bat as follows:

@echo off

set "username=%~1"

REM Do NOT strip quotes from the password argument
set password=%2

REM Using a trick with set /p, echo the password unquoted.
REM Note: Put the "|" directly after the ":" to avoid trailing spaces.
<NUL set /p=%password% & echo:|python main.py

The workaround repurposes set's /p option, which accepts a prompt message to print when interactively prompting the user for a value and prints the message without quotes; the actual interactive prompt is suppressed via <NUL, so that only the message is printed.

echo: prints a newline (line break), which is necessary, because the set /p command prints its message without a trailing newline (if you don't want to send a newline, simply omit & echo:).

Caveat: In addition to the problem with embedded "" applying here too, this workaround has a side effect: it trims leading whitespace; e.g., " foo " results in output foo   (only trailing whitespace is preserved); however, given that arguments with leading whitespace are rare, this may not matter in practice.


Given how cumbersome / obscure the above approaches are, the stdin-based approach shown at the top is preferable.

like image 29
mklement0 Avatar answered Oct 04 '22 10:10

mklement0


You pass arguments to batch files in powershell using the -argumentlist switch of start/saps. For you you could use:

saps "$script" -argumentlist $args

But I would suggest first breaking $args up as it may not work since to pass arguments, you usually want to pass the arguments one at a time like:

saps "$script" -argumentlist "1","2","3"

Passing $args will work most of the time, but there are some cases where where it won't work. Most of the time you are fine though

like image 35
Nico Nekoru Avatar answered Oct 04 '22 10:10

Nico Nekoru