| Title | Bentoml OpenLLM 0.6.30 Command Injection |
|---|
| Description | https://github.com/bentoml/OpenLLM
OpenLLM uses `asyncio.create_subprocess_shell` to launch model servers, constructing the shell command by joining a Python list into a single string with `' '.join(...)`. Model version and name identifiers are taken directly from directory names in a cloned model repository without any sanitization. An attacker who controls a model repository can name a directory to contain shell metacharacters (for example `;`, `$()`, `` ` ``), causing arbitrary commands to execute on the victim's machine when the victim runs `openllm run` or `openllm serve` using the attacker-controlled repository.
### Details
The root cause is in `src/openllm/common.py` at the `async_run_command` function (lines 425-431):
```python
proc = await asyncio.create_subprocess_shell(
' '.join(map(str, cmd)),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=cwd,
env=env,
)
```
The `cmd` list is joined into a single string and handed to a shell interpreter. Any shell metacharacters present in the joined string are evaluated by the shell.
The `cmd` list is built in `src/openllm/local.py` at `_get_serve_cmd` (lines 31-44):
```python
def _get_serve_cmd(bento, port=3000, cli_args=None):
cmd = ['bentoml', 'serve', bento.bentoml_tag]
if port != 3000:
cmd += ['--port', str(port)]
if cli_args:
for arg in cli_args:
cmd += ['--arg', arg]
return cmd, EnvVars(...)
```
`bento.bentoml_tag` is defined in `src/openllm/common.py` (line 179):
```python
@property
def bentoml_tag(self) -> str:
return f'{self.path.parent.name}:{self.path.name}'
```
`self.path` is a `pathlib.Path` pointing to the directory of the model version inside the cloned repository. `self.path.parent.name` is the model name directory and `self.path.name` is the version directory. Neither is sanitized or quoted before being inserted into the list that is subsequently joined for the shell.
The contrast with safe usage is visible in the same function: the `shlex.quote` call at line 405 is used only for cosmetic display output, not for the actual subprocess invocation:
```python
output(f'$ export {k}={shlex.quote(v)}', style='orange') # display only -- not the real command
```
The synchronous `run_command` function (also in common.py) uses `subprocess.run(cmd, ...)` with a list (no shell=True), so it is not vulnerable. Only `async_run_command`, called exclusively from the `openllm run` path, is affected. |
|---|
| Source | ⚠️ https://github.com/bentoml/OpenLLM/issues/1229 |
|---|
| User | geochen (UID 78995) |
|---|
| Submission | 06/07/2026 10:18 (1 month ago) |
|---|
| Moderation | 07/08/2026 09:41 (1 month later) |
|---|
| Status | Accepted |
|---|
| VulDB entry | 376786 [bentoml OpenLLM 0.6.30 Model Repository Directory Name src/openllm/common.py async_run_command cmd command injection] |
|---|
| Points | 20 |
|---|