Introduction
In the project/website/cmds/http_admin/cgi-bin/cgi_cmd_wrapper file, we discovered a critical command injection vulnerability that could have allowed remote attackers to execute arbitrary commands on the server. This CGI script processes HTTP requests and constructs shell commands from user-supplied parameters—a dangerous pattern that opened the door to complete system compromise.
The vulnerable code on line 22 used subprocess.check_output(cmd, shell=True) where the cmd variable was built directly from HTTP query parameters. This meant any attacker who could send an HTTP request to this endpoint could inject shell metacharacters and run any command they wanted on the server.
The Vulnerability Explained
The CGI script was designed to execute commands based on HTTP parameters. Here's the vulnerable code pattern:
form = cgi.FieldStorage()
cmd = "cgi-bin/" + form['c'].value
for item in cgi.parse().keys():
if item != 'c':
cmd += ' ' + form[item].value
result = subprocess.check_output(cmd, shell=True).decode("utf-8")
The critical flaw lies in how the command string is constructed and executed:
- String concatenation with user input: The
cmdvariable starts with a prefix but then appends raw user input fromform['c'].valueand other form parameters - No input sanitization: User-supplied values are directly concatenated without any validation or escaping
- Shell execution enabled: The
shell=Trueparameter tells Python to pass the command through a shell interpreter, which processes metacharacters
Attack Scenario
An attacker could craft a malicious HTTP request like:
GET /cgi-bin/cgi_cmd_wrapper?c=status;cat%20/etc/passwd HTTP/1.1
When this request is processed:
- The cmd variable becomes: cgi-bin/status;cat /etc/passwd
- The shell interprets the semicolon as a command separator
- First cgi-bin/status executes, then cat /etc/passwd executes
- The attacker receives the contents of /etc/passwd in the response
Other injection techniques work equally well:
- Pipe injection: ?c=status|id executes id and shows user context
- Backtick injection: ?c=status\whoami`executeswhoamiinline
- **Command substitution**:?c=status$(rm -rf /)` could delete the entire filesystem
The impact is complete server compromise—attackers could read sensitive files, install backdoors, pivot to internal networks, or destroy data.
The Fix
The fix implements two crucial changes that eliminate the command injection vector:
Before (Vulnerable)
cmd = "cgi-bin/" + form['c'].value
for item in cgi.parse().keys():
if item != 'c':
cmd += ' ' + form[item].value
result = subprocess.check_output(cmd, shell=True).decode("utf-8")
After (Secure)
cmd = ["cgi-bin/" + form['c'].value]
for item in cgi.parse().keys():
if item != 'c':
cmd.append(form[item].value)
result = subprocess.check_output(cmd, shell=False).decode("utf-8")
Why This Fix Works
-
Command as a list: By converting
cmdfrom a string to a list, each argument is treated as a discrete element. The first element is the executable, and subsequent elements are its arguments. -
shell=False: With this setting, Python's subprocess module executes the command directly without invoking a shell interpreter. Shell metacharacters like;,|, and backticks are treated as literal characters, not as operators. -
Argument isolation: When an attacker tries
?c=status;id, the entire stringstatus;idbecomes a single argument tocgi-bin/. The shell never sees it, so the semicolon has no special meaning—it's just part of a filename that likely doesn't exist.
The same attack request now results in:
- cmd = ["cgi-bin/status;id"]
- Python tries to execute a file literally named cgi-bin/status;id
- This file doesn't exist, so the command fails safely
- No injection occurs
Prevention & Best Practices
1. Always Use shell=False with Argument Lists
# DANGEROUS - Never do this with user input
subprocess.run(f"process {user_input}", shell=True)
# SAFE - Arguments are isolated
subprocess.run(["process", user_input], shell=False)
2. Validate and Whitelist Input
Even with shell=False, validate that input matches expected patterns:
import re
ALLOWED_COMMANDS = {'status', 'info', 'health'}
if form['c'].value not in ALLOWED_COMMANDS:
raise ValueError("Invalid command")
3. Use shlex.quote() When Shell is Unavoidable
If you absolutely must use shell=True (which you almost never do), escape user input:
import shlex
safe_input = shlex.quote(user_input)
4. Apply Principle of Least Privilege
Run CGI scripts with minimal permissions. Use dedicated service accounts that can only access required resources.
5. Use Security Linters
Tools like Bandit can catch these patterns:
bandit -r your_project/
Bandit rule B602 specifically flags subprocess calls with shell=True.
Key Takeaways
- Never use
shell=Truewith any data derived from HTTP parameters—thecgi_cmd_wrapperscript demonstrated how trivially this can be exploited - String concatenation for command building is inherently dangerous—the original code's
cmd += ' ' + form[item].valuepattern is a red flag - Converting commands to lists is a structural fix, not just a workaround—it changes how the operating system interprets the input
- CGI scripts are particularly high-risk because they're directly network-accessible and designed to process untrusted input
- Regression tests should verify injection resistance—the added test suite validates that semicolons, pipes, and backticks don't enable command execution
How Orbis AppSec Detected This
- Source: HTTP request parameters via
cgi.FieldStorage()andcgi.parse()in the CGI script - Sink:
subprocess.check_output(cmd, shell=True)at line 22 ofcgi_cmd_wrapper - Missing control: No input validation, sanitization, or safe subprocess invocation pattern
- CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)
- Fix: Converted command string to argument list and changed
shell=Truetoshell=False
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
This command injection vulnerability in cgi_cmd_wrapper represents one of the most dangerous patterns in web application security—taking user input and passing it directly to a shell. The fix demonstrates the correct approach: use subprocess with shell=False and pass arguments as a list, ensuring that shell metacharacters are never interpreted.
When reviewing code that executes system commands, always ask: "Can an attacker control any part of this command string?" If the answer is yes, restructure the code to use argument lists and disable shell interpretation. This structural change is more robust than any amount of input filtering.