Skip to content

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