Dangerous asyncio Shell Execution

HIGH

asyncio.create_subprocess_shell() passes the command through the system shell, enabling command injection when arguments contain untrusted input.

Rule Information

Language
Python
Category
Python Core
Author
Shivasurya
Shivasurya
Last Updated
2026-03-22
Tags
pythonasyncioshellcommand-injectionasyncCWE-78OWASP-A03
CWE References

Interactive Playground

Experiment with the vulnerable code and security rule below. Edit the code to see how the rule detects different vulnerability patterns.

pathfinder scan --ruleset python/PYTHON-LANG-SEC-022 --project .
1
2
3
4
5
6
7
rule.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

About This Rule

Understanding the vulnerability and how it is detected

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.

Security Implications

Potential attack scenarios if this vulnerability is exploited

1

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.

2

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.

3

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.

4

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.

How to Fix

Recommended remediation steps

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

Detection Scope

How Code Pathfinder analyzes your code for this vulnerability

This rule detects calls to asyncio.create_subprocess_shell() and the equivalent asyncio.subprocess.create_subprocess_shell() form. All call sites are flagged since this function always invokes the system shell and is inherently riskier than asyncio.create_subprocess_exec().

Compliance & Standards

Industry frameworks and regulations that require detection of this vulnerability

CWE Top 25
CWE-78 ranked #5 in 2023 Most Dangerous Software Weaknesses
OWASP Top 10
A03:2021 - Injection
NIST SP 800-53
SI-10: Information Input Validation
PCI DSS v4.0
Requirement 6.2.4 - Protect web-facing applications against injection attacks

References

External resources and documentation

Similar Rules

Explore related security rules for Python

Frequently Asked Questions

Common questions about Dangerous asyncio Shell Execution

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.
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.
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.
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.
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.
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.

New feature

Get these findings posted directly on your GitHub pull requests

The Dangerous asyncio Shell Execution rule runs in CI and posts inline review comments on the exact lines — no dashboard, no SARIF viewer.

See how it works