# PYTHON-LANG-SEC-022: Dangerous asyncio Shell Execution

> **Severity:** HIGH | **CWE:** CWE-78 | **OWASP:** A03:2021

- **Language:** Python
- **Category:** Python Core
- **URL:** https://codepathfinder.dev/registry/python/lang/PYTHON-LANG-SEC-022
- **Detection:** `pathfinder scan --ruleset python/PYTHON-LANG-SEC-022 --project .`

## Description

Python's asyncio.create_subprocess_shell() is the async equivalent of subprocess.run()
with shell=True. It passes the command string to the system shell (/bin/sh on Unix) for
interpretation, which means all shell metacharacters are processed before the command
executes.

When any part of the command string is derived from untrusted input such as HTTP request
parameters or user-provided data, an attacker can inject shell metacharacters to execute
additional commands or access the shell's full feature set.

The safe replacement is asyncio.create_subprocess_exec() which accepts a list of arguments
and passes them directly to the OS exec() syscall without shell interpretation, exactly
like subprocess.run() with shell=False.


## Vulnerable Code

```python
import subprocess
import asyncio

# SEC-022: asyncio shell
async def run_cmd():
    proc = await asyncio.create_subprocess_shell("ls -la")
    await proc.communicate()
```

## Secure Code

```python
import asyncio

# INSECURE: asyncio.create_subprocess_shell() with user input
# proc = await asyncio.create_subprocess_shell(f"process {user_input}")

# SECURE: asyncio.create_subprocess_exec() with list arguments
async def run_processor(input_file: str) -> str:
    import re
    if not re.match(r'^[a-zA-Z0-9_\-\.]+$', input_file):
        raise ValueError(f"Invalid filename: {input_file}")
    proc = await asyncio.create_subprocess_exec(
        "/usr/bin/processor",
        "--input", input_file,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30)
    if proc.returncode != 0:
        raise RuntimeError(f"Processor failed: {stderr.decode()}")
    return stdout.decode()

# SECURE: With timeout and error handling
async def convert_file(source: str, dest: str) -> None:
    import re
    SAFE_PATTERN = re.compile(r'^[a-zA-Z0-9_\-\.]+$')
    if not SAFE_PATTERN.match(source) or not SAFE_PATTERN.match(dest):
        raise ValueError("Invalid filename")
    proc = await asyncio.create_subprocess_exec(
        "/usr/bin/convert", source, dest,
        stdout=asyncio.subprocess.DEVNULL,
        stderr=asyncio.subprocess.PIPE,
    )
    try:
        _, stderr = await asyncio.wait_for(proc.communicate(), timeout=60)
    except asyncio.TimeoutError:
        proc.kill()
        raise

```

## Detection Rule (Python SDK)

```python
from rules.python_decorators import python_rule
from codepathfinder import calls, flows, QueryType
from codepathfinder.presets import PropagationPresets

class AsyncioModule(QueryType):
    fqns = ["asyncio"]


@python_rule(
    id="PYTHON-LANG-SEC-022",
    name="Dangerous asyncio Shell Execution",
    severity="HIGH",
    category="lang",
    cwe="CWE-78",
    tags="python,asyncio,shell,command-injection,CWE-78",
    message="asyncio.create_subprocess_shell() detected. Use create_subprocess_exec() instead.",
    owasp="A03:2021",
)
def detect_asyncio_shell():
    """Detects asyncio.create_subprocess_shell and related methods."""
    return AsyncioModule.method("create_subprocess_shell")
```

## How to Fix

- Replace asyncio.create_subprocess_shell() with asyncio.create_subprocess_exec() and pass the command as separate arguments to avoid shell interpretation.
- Validate all user-controlled values that become process arguments against a strict allowlist or regex before use.
- Set a timeout using asyncio.wait_for() to prevent hung child processes from blocking the event loop.
- Capture stdout and stderr explicitly to prevent sensitive information from appearing in server logs.
- Use absolute executable paths to prevent PATH hijacking attacks in the async context.

## Security Implications

- **Async Shell Command Injection:** The same shell injection risks present in subprocess with shell=True apply to
asyncio.create_subprocess_shell(). Semicolons, pipes, backticks, and dollar signs
in user input allow injection of additional commands executed with the process's
privileges.

- **Concurrent Injection Amplification:** In async web servers handling concurrent requests, a shell injection vulnerability in
an async subprocess call can be exploited by multiple concurrent attackers simultaneously,
amplifying the impact compared to synchronous code.

- **Event Loop Blocking from Injected Commands:** Injected long-running or CPU-intensive commands can block the asyncio event loop,
causing denial-of-service for all concurrent requests handled by the same event loop.

- **Uncaptured Output in Async Context:** In async code, subprocess output handling is more complex and errors in output
capture can lead to process hangs or output appearing in unexpected contexts,
potentially exposing sensitive information.


## FAQ

**Q: What is the async equivalent of subprocess.run() with shell=False?**

asyncio.create_subprocess_exec() is the async equivalent of subprocess.run() with
shell=False. It accepts the executable as the first argument and additional arguments
as separate positional parameters, bypassing the shell entirely. Use it instead of
create_subprocess_shell() for all async process creation.


**Q: Does asyncio.create_subprocess_shell() have the same risks as subprocess shell=True?**

Yes, exactly the same risks apply. Both pass the command to /bin/sh (or cmd.exe on
Windows) for interpretation. Shell metacharacters in user input are expanded and executed.
The async nature does not change the injection vulnerability.


**Q: How do I handle shell pipelines in async code?**

Implement pipelines using asyncio.create_subprocess_exec() for each process in the
pipeline, connected via asyncio.subprocess.PIPE for stdin/stdout. Alternatively,
process intermediate data in Python async code between exec() calls. Avoid piping
commands through a shell string.


**Q: Can blocking in an injected command affect other async requests?**

Yes. If an injected command runs a CPU-intensive or long-running operation, and
asyncio.wait_for() is not used, the event loop can block, causing all concurrent
requests in the same process to queue up. Always use asyncio.wait_for() with a
timeout to limit the maximum duration of subprocess execution.


**Q: Is asyncio.create_subprocess_shell() ever acceptable to use?**

Only when the entire command string is a hardcoded literal with no user-controlled
components. Even then, asyncio.create_subprocess_exec() with a list is preferable
for clarity and to prevent future developers from accidentally adding user input
to the command string.


**Q: How do I migrate existing create_subprocess_shell() calls to create_subprocess_exec()?**

Replace await asyncio.create_subprocess_shell("cmd arg1 arg2") with
await asyncio.create_subprocess_exec("cmd", "arg1", "arg2"). For more complex
shell commands, use shlex.split() to tokenize the hardcoded command string into
a list, then validate all non-hardcoded arguments before adding them to the list.


## References

- [CWE-78: OS Command Injection](https://cwe.mitre.org/data/definitions/78.html)
- [Python docs: asyncio.create_subprocess_shell()](https://docs.python.org/3/library/asyncio-subprocess.html#asyncio.create_subprocess_shell)
- [Python docs: asyncio.create_subprocess_exec()](https://docs.python.org/3/library/asyncio-subprocess.html#asyncio.create_subprocess_exec)
- [OWASP OS Command Injection Defense Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html)
- [OWASP Top 10 A03:2021 Injection](https://owasp.org/Top10/A03_2021-Injection/)

---

Source: https://codepathfinder.dev/registry/python/lang/PYTHON-LANG-SEC-022
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
