Repository: xonsh/xonsh

Issue:
A `cmd.bat` in current directory could trick xonsh to run arbitrary code on Windows
<!--- Provide a general summary of the issue in the Title above -->
<!--- If you have a question along the lines of "How do I do this Bash command in xonsh"
please first look over the Bash to Xonsh translation guide: https://xon.sh/bash_to_xsh.html
If you don't find an answer there, please do open an issue! -->

## xonfig

<details>

```
+------------------+-----------------------+
| xonsh            | 0.14.3                |
| Python           | 3.10.11               |
| PLY              | 3.11                  |
| have readline    | False                 |
| prompt toolkit   | None                  |
| shell type       | readline              |
| history backend  | json                  |
| pygments         | None                  |
| on posix         | False                 |
| on linux         | False                 |
| on darwin        | False                 |
| on windows       | True                  |
| on cygwin        | False                 |
| on msys2         | False                 |
| is superuser     | False                 |
| default encoding | utf-8                 |
| xonsh encoding   | utf-8                 |
| encoding errors  | surrogateescape       |
| xontrib          | []                    |
| RC file 1        | C:\Users\xzc/.xonshrc |
+------------------+-----------------------+
```
There is RC file here but it should not matter:
```
# XONSH WEBCONFIG START
$PROMPT = '{env_name}{BOLD_GREEN}{user}@{hostname}{BOLD_BLUE} {cwd}{branch_color}{curr_branch: {}}{RESET} {RED}{last_return_code_if_nonzero:[{BOLD_INTENSE_RED}{}{RED}] }\n{RESET}{BOLD_BLUE}{prompt_end}{RESET} '
# XONSH WEBCONFIG END

# my customs
aliases['ls'] = ['D:\\msys64\\usr\\bin\\ls.exe', '--color']
aliases['ll'] = ['D:\\msys64\\usr\\bin\\ls.exe', '--color', '-lh']
aliases['file'] = ['D:\\msys64\\usr\\bin\\file.exe']
```

</details>

## Expected Behavior
<!--- Tell us what should happen -->
On Windows, you create a `cmd.bat` in your current directory (in this example that is `d:\xzc\lab\xonsh_debug\xonsh-0.14.3`) saying:
```
echo "hopefully this code is not malicious"
```
and run anything that is an alias to `cmd /c `. For example `mklink`:
```
mklink /?
```

I expect to see the output of the help on mklink. On a Chinese version of Windows, that is:
```
创建符号链接。

MKLINK [[/D] | [/H] | [/J]] Link Target

        /D      创建目录符号链接。默认为文件
                符号链接。
        /H      创建硬链接而非符号链接。
        /J      创建目录联接。
        Link    指定新的符号链接名称。
        Target  指定新链接引用的路径
                (相对或绝对)。
```

## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->
<!--- If part of your bug report is a traceback, please first enter debug mode before triggering the error
To enter debug mode, set the environment variable `XONSH_DEBUG=1` _before_ starting `xonsh`.
On Linux and OSX, an easy way to to do this is to run `env XONSH_DEBUG=1 xonsh` -->
Instead of giving me help on mklink, I was presented the execution output of `cmd.bat`:
```
"hopefully this code is not malicious"
```
This picture showing the debugging scene of this issue could be clearer:
![xonsh_run_cmd bat](https://github.com/xonsh/xonsh/assets/33486158/6520be90-1f8a-42df-822e-cdf2b75bb141)


### Traceback (if applicable)
No traceback since there is no exception thrown.

## Steps to Reproduce
<!--- Please try to write out a minimal reproducible snippet to trigger the bug, it will help us fix it! -->
Sorry but I have already shown how to reproduce in previous sections

## Side note
This issue occurred when I tried to run `dir .` in `D:\msys64\usr\bin\` and I kept receiving
```
Traceback (most recent call last):
  File "C:\Users\xzc\AppData\Local\Programs\Python\Python310\lib\site-packages\xonsh\procs\pipelines.py", line 166, in __init__
    proc = spec.run(pipeline_group=pipeline_group)
  File "C:\Users\xzc\AppData\Local\Programs\Python\Python310\lib\site-packages\xonsh\procs\specs.py", line 455, in run
    p = self._run_binary(kwargs)
  File "C:\Users\xzc\AppData\Local\Programs\Python\Python310\lib\site-packages\xonsh\procs\specs.py", line 473, in _run_binary
    p = self.cls(cmd, bufsize=bufsize, **kwargs)
  File "C:\Users\xzc\AppData\Local\Programs\Python\Python310\lib\subprocess.py", line 971, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "C:\Users\xzc\AppData\Local\Programs\Python\Python310\lib\subprocess.py", line 1456, in _execute_child
    hp, ht, pid, tid = _winapi.CreateProcess(executable, args,
OSError: [WinError 193] %1 不是有效的 Win32 应用程序。
```
It turns out that there is a `cmd` in `D:\msys64\usr\bin\` that leads to this very issue.

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**



Discussion:
I haven't checked if xonsh uses subprocess.run with shell=True, but does this still happen in Python 3.12?  

It reminds me of this bugfix:  


_Changed in version 3.12: Changed Windows shell search order for shell=True. The current directory and %PATH% are replaced with %COMSPEC% and %SystemRoot%\System32\cmd.exe. As a result, dropping a malicious program named cmd.exe into a current directory no longer works._


https://docs.python.org/3/library/subprocess.html
> does this still happen in Python 3.12?

Sadly it DOES happen in Python 3.12:

<details>

```powershell
(xonsh_dbg_py312) PS D:\xzc\lab\xonsh_debug\xonsh-0.14.3> Get-Content .\cmd.bat
echo "hopefully this code is not malicious"
(xonsh_dbg_py312) PS D:\xzc\lab\xonsh_debug\xonsh-0.14.3> python --version
Python 3.12.1
(xonsh_dbg_py312) PS D:\xzc\lab\xonsh_debug\xonsh-0.14.3> $Env:XONSH_DEBUG=1
(xonsh_dbg_py312) PS D:\xzc\lab\xonsh_debug\xonsh-0.14.3> xonsh
<xonsh-code>:1:0 - cls
<xonsh-code>:1:0 + ![cls]
Skipping setup. Because no `readline` implementation available.
            Please install a backend (`readline`, `prompt-toolkit`, etc) to use
            `xonsh` interactively.
            See https://github.com/xonsh/xonsh/issues/1170
hello!
xonsh_dbg_py312xzc@Lenovo-Win11-xzc D:\xzc\lab\xonsh_debug\xonsh-0.14.3
@ mklink /?
<stdin>:1:8 - mklink /?
<stdin>:1:8 + ![mklink /?]

D:\xzc\lab\xonsh_debug\xonsh-0.14.3>echo "hopefully this code is not malicious"
"hopefully this code is not malicious"
xonsh_dbg_py312xzc@Lenovo-Win11-xzc D:\xzc\lab\xonsh_debug\xonsh-0.14.3
@ xonfig
<xonsh-code>:1:0 - xonfig
<xonsh-code>:1:0 + ![xonfig]
+------------------+-----------------------+
| xonsh            | 0.14.3                |
| Python           | 3.12.1                |
| PLY              | 3.11                  |
| have readline    | False                 |
| prompt toolkit   | None                  |
| shell type       | readline              |
| history backend  | json                  |
| pygments         | None                  |
| on posix         | False                 |
| on linux         | False                 |
| on darwin        | False                 |
| on windows       | True                  |
| on cygwin        | False                 |
| on msys2         | False                 |
| is superuser     | False                 |
| default encoding | utf-8                 |
| xonsh encoding   | utf-8                 |
| encoding errors  | surrogateescape       |
| xontrib          | []                    |
| RC file 1        | C:\Users\xzc/.xonshrc |
+------------------+-----------------------+
```

</details>

> I haven't checked if xonsh uses subprocess.run with shell=True

I searched for `shell=` and `.run` in the source but didn't find that. I don't know if I missed some class defaults though.
Hiya,

I've gotten to the bottom of it.  

Basically the problem is in Xonsh's prefilled default aliases on Windows, which include mklink but also dir:

A fix could be as simple as replacing the string literal `"cmd"` with `os.getenv('COMSPEC')`

```
    if ON_WINDOWS:
        # Borrow builtin commands from cmd.exe.
        windows_cmd_aliases = {
            "cls",
            "copy",
            "del",
            "dir",
            "echo",
            "erase",
            "md",
            "mkdir",
            "mklink",
            "move",
            "rd",
            "ren",
            "rename",
            "rmdir",
            "time",
            "type",
            "vol",
        }
        for alias in windows_cmd_aliases:
            default_aliases[alias] = ["cmd", "/c", alias]
```
https://github.com/xonsh/xonsh/blob/e3a6dcf255a7f25d729df2b3da3740475f3bb309/xonsh/aliases.py#L875

Proof:

Run a command that isn't in the aliases dict, e.g. `w32tm /tz` (try to only pick examples that are harmless https://ss64.com/nt/), or make another batch file (and call it from xonsh) in the same directory as the `cmd.bat` imposter.  The attacker's `cmd.bat` is not called.

```
(xonsh) ~\Coding\delete @ mklink /?

~\Coding\delete>echo "hopefully this code is not malicious"
"hopefully this code is not malicious"
(xonsh) ~\Coding\delete @ dir /A

~\Coding\delete>echo "hopefully this code is not malicious"
"hopefully this code is not malicious"
(xonsh) ~\Coding\delete @ w32tm /tz
Time zone: Current:TIME_ZONE_ID_STANDARD Bias: 0min (UTC=LocalTime+Bias)
  [Standard Name:"GMT Standard Time" Bias:0min Date:(M:10 D:5 DoW:0)]
  [Daylight Name:"GMT Daylight Time" Bias:-60min Date:(M:3 D:5 DoW:0)]
(xonsh) ~\Coding\delete @ legit_user_file

~\Coding\delete>echo A user's custom batch file.
A user's custom batch file.
(xonsh) ~\Coding\delete @

```


FYI, Xonsh uses a bunch of suprocess.capture_output s (and the other olf one) but for its main external calls it runs subprocess.POpen, via a custom class and the entire AST and parser.  It took me a long time to figure that out, and will also take a long time to explain the details.  
With this line changed: https://github.com/xonsh/xonsh/blob/e3a6dcf255a7f25d729df2b3da3740475f3bb309/xonsh/aliases.py#L897

```
        for alias in windows_cmd_aliases:
            # default_aliases[alias] = ["cmd", "/c", alias]
            default_aliases[alias] = [os.getenv("COMSPEC"), "/c", alias]
```

this is the new, hopefully safer, behaviour:

```
(xonsh)  ~\Coding\delete @ mklink /?
Creates a symbolic link.

MKLINK [[/D] | [/H] | [/J]] Link Target

        /D      Creates a directory symbolic link.  Default is a file
                symbolic link.
        /H      Creates a hard link instead of a symbolic link.
        /J      Creates a Directory Junction.
        Link    Specifies the new symbolic link name.
        Target  Specifies the path (relative or absolute) that the new link
                refers to.
(xonsh)  ~\Coding\delete @ dir /A
 Volume in drive C is Windows-SSD
 Volume Serial Number is 

 Directory of ~\Coding\delete

24/12/2023  16:08    <DIR>          .
12/12/2023  12:03    <DIR>          ..
24/12/2023  13:43                43 cmd.bat
24/12/2023  16:06                32 legit_user_file.bat
12/11/2023  20:57    <DIR>          Project1
               2 File(s)             75 bytes
               3 Dir(s)  lots bytes free
```

Resolving patch:
diff --git a/xonsh/aliases.py b/xonsh/aliases.py
--- a/xonsh/aliases.py
+++ b/xonsh/aliases.py
@@ -894,7 +894,7 @@ def make_default_aliases():
             "vol",
         }
         for alias in windows_cmd_aliases:
-            default_aliases[alias] = ["cmd", "/c", alias]
+            default_aliases[alias] = [os.getenv("COMSPEC"), "/c", alias]
         default_aliases["call"] = ["source-cmd"]
         default_aliases["source-bat"] = ["source-cmd"]
         default_aliases["clear"] = "cls"


Test patch: