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
3 Answers 3
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 %^&<>|'\`,;=(\)![]\/
3 Comments
set/p= workaround; slight simplification: <NUL set/p="%arg%"|...; if you do want a trailing newline: <NUL set/p="%arg%" & echo:|...-NoNewWindow without -Wait, any output from the batch file invoked will arrive asynchronously, probably causing confusion. Conversely: invoking a batch file via Start-Process is probably the wrong approach to begin with. """$pass""" is effectively the same as "`"$unsecurePassword`"" from the question (difference in variable names aside): while PowerShell's general-purpose escape character is `, " in particular can alternatively be escaped as "" inside a "..." string." characters when calling external programs aside - see this answer). On the cmd.exe (batch-file) side: by enclosing the argument in the PowerShell call in "...", the argument is passed safely. Using your set /p workaround to print the argument unquoted also makes escaping inside the batch file unnecessary.tl;dr:
Unless you specifically need the batch file to run in a new window, avoid
Start-Process(whose built-in aliases arestartandsaps), 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 yourpythonscript:
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
echoan 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.
Comments
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
2 Comments
-ArgumentList parameter is implied for the second positional argument, your solution is effectively the same as the OP's own attempt, start-process $script $args (leaving aside that the double quotes around $script in your solution are redundant). Aside from that, there is no difference between passing an array of arguments via a variable ($args) vs. via a literal array ("1","2","3").Start-Process $script 'one', 'two (2)', use Start-Process $script "one `"two (2)`""
$argsis an automatic variable, so you should avoid its use as a custom variable.-ArgumentListparameter is implied for the second positional argument; that is,start-process $script $argsandsaps "$script" -argumentlist $argsare equivalent (sapsis a built-in alias forStart-Process; the double quotes around$scriptare redundant).