Command Injection¶
Overview¶
If a shell command is improperly derived from user input, an attacker is likely to be able to execute arbitrary commands on the host running the API. The commands are usually executed with the privileges of the vulnerable API.
Command injections are severe security vulnerabilities, leading to data leaks and arbitrary code security.
Recommendation¶
Prefer using existing APIs in your languages or library ecosystem instead of
executing a subprocess. For instance, rather than execute curl
, consider
using a http client library in your language.
If the above is not an option, prefer using a library function that takes a
list of arguments instead of a single space-delimited string. This avoids
unexpected shell evaluation due to special character sequences such as $()
,
;
and `.
We also recommend sanitizing the user input as much as possible while maintaining the desired functionality of your endpoint. For instance, you could consider disallowing non alphanumeric characters when the input is used as part of a command.
Examples¶
In the following example, we have a simple FastAPI python file upload API. The API uses a folder to store the files, and users can upload, retrieve or delete files:
from fastapi import FastAPI, File, UploadFile, HTTPException
from pathlib import Path
from os import system
app = FastAPI()
STORAGE = Path("/tmp")
@app.get("/files/{file_id}")
async def read_file(file_id: int):
try:
return {"contents": open(STORAGE / str(file_id), "rb").read()}
except FileNotFoundError:
raise HTTPException(status_code=404, detail="😱 File not found")
@app.post("/files/{file_id}")
async def write_file(file_id: int, file: UploadFile = File(...)):
contents = await file.read()
open(STORAGE / str(file_id), "wb").write(contents)
return "🗄️ File has been stored"
@app.delete("/files/{file_id}")
async def delete_file(file_id):
system("rm " + str(STORAGE / file_id)) # this is bad
return "✨ File has been deleted"
As you can see, the delete endpoint is using system
to execute rm
to remove
the file. However, an attacker may pass a command as part of file_id
:
$ curl 'http://localhost:8081/files/;echo%20hello%20from%20command%20injection' -XDELETE 16:40:27
"✨ File has been deleted"
Looking at the log, we see something unusual: rm
returned an error, and the output of the
attacker command:
rm: cannot remove '/tmp/': Is a directory
hello from command injection
INFO: 127.0.0.1:54716 - "DELETE /files/%3Becho%20hello%20from%20command%20injection HTTP/1.1" 200 OK
The attacker executed an arbitrary command: echo
. This command is benign, but
you could imagine an attacker could just as easily exfiltrate data or open up a
shell for themselves.
There are multiple ways to remediate the issues, following the recommendations
above:
- Replacing os.system
with subprocess.check_output(["rm", str(STORAGE
/ file_id)])
. This prevents the injection (but might is susceptible to path
traversal)
- Even better, replacing os.system
with os.remove
, a library function
designed to delete a file. This prevents having to start a subprocess, which is
safer and faster.
- Validating file_id
to ensure that it cannot include a command
injection payload, or a path traversal for that matter.
A safe deletion endpoint may look like:
@app.delete("/files/{file_id}")
async def delete_file(file_id: int):
os.remove(STORAGE / str(file_id)) # this is ok because file_id is an integer
return "✨ File has been deleted"
References¶
- OWASP: Command Injection.
- Common Weakness Enumeration: CWE-77.
- Common Weakness Enumeration: CWE-78.