The Fix Landed in the Other Template: BentoML’s Third Dockerfile Injection and Four More AI-Serving Records Nobody Can Install

Five CVEs reached NVD between 14:17:03Z and 14:17:04Z on 19 October 2026 — eighty seconds, all from disclosure@vulncheck.com — against three projects that sit directly under production model serving: BentoML (CVE-2026-112264, CVSS 4.0 8.5), LangBot (CVE-2026-112265, 8.3), and SGLang and its router (CVE-2026-112266 at 8.2, CVE-2026-112267 at 6.9, CVE-2026-112268 at 5.3). Taken one at a time they are an ordinary Tuesday. Taken together they share a fact that does not appear in any of the five descriptions: not one of them has a remediation you can install today. One has no fix anywhere, two have fixes merged to a branch no release has been cut from, and one has a fix sitting in an unmerged pull request. We read all five NVD records, the reporters’ write-ups, the vulnerable source at the named tags, the fix diffs and the release histories first-hand.

BentoML: the same bug, the third time, in the template nobody migrated off

CVE-2026-112264 is Dockerfile instruction injection through docker.env dictionary keys. The mechanism takes four lines to state, and all four are verifiable at the tagged release.

BentoML’s legacy container template, base.j2, renders the environment mapping like this — quoted verbatim from v1.4.39, lines 45–49:

{% if __options__env is not none %}
{% for key, value in __options__env.items() -%}
ARG {{ key }}={{ value }}
ENV {{ key }}=${{ key }}
{% endfor -%}

The key is interpolated raw, in the position of a Dockerfile directive. Upstream, _convert_env in src/bentoml/_internal/bento/build_config.py validates the list form of env against a regex (^(\w+)=(?:[\.\w\-,\/]+)$) and raises on anything that fails — and then, forty lines later, handles the dict form in its entirety as return {str(k): str(v) for k, v in env.items()}. One surface of the same option is validated; its sibling is not. A newline in a dict key therefore reaches Jinja2 intact and breaks out of the ARG line into new Dockerfile instructions, which docker build executes on the host running bentoml containerize.

What makes this a third-generation finding rather than a first is the sanitiser that already exists in the codebase. The modern template, base_v2.j2, renders the same construct at lines 70 and 72 as ARG {{ env.name|normalize_line }} and ENV {{ env.name|normalize_line }}=${{ env.name|normalize_line }}. normalize_line is defined in generate.py as " ".join(s.strip().split()) — which collapses newlines, and is precisely the defence the legacy template lacks. That filter was added in 1.4.39 to fix CVE-2026-44346. BentoML’s own advisory for that record is titled, verbatim, “Dockerfile command injection via envs[*].name in bentofile.yaml (sibling fix-bypass of CVE…)” — and the advisory published one minute earlier, CVE-2026-44345, covers the same injection through docker.base_image. The project has now shipped three advisories for Dockerfile injection in template rendering plus a fourth (CVE-2026-35043) explicitly titled an incomplete fix for CVE-2026-33744. The maintainers are using the words “sibling” and “incomplete fix” in their own advisory titles. The class is understood. The enumeration of rendering sites is what keeps coming up short.

Two details sharpen the practical risk. First, base.j2 is not dead code: generate_containerfile selects it, in a branch commented “If base_image is specified, then use the base template instead”, whenever docker.base_image is set — which is exactly what an organisation does when it standardises on an approved internal base image. The hardened path is the default; the legacy path is the one compliance pushes you onto. Second, the reporter notes that the Jinja2 environment is a SandboxedEnvironment, so this is not template-engine code execution; the sandbox held. The injection targets the generated Dockerfile instead, and the sandbox has nothing to say about that. A control doing its job perfectly, one layer below the layer where the decision actually matters.

The timeline is the part we would want a maintainer to answer. George Chen’s write-up opens with two lines before the technical content: reported by email on 2 June 2026, no response; reported via Huntr on 7 June 2026, no response. That is 139 days to a CVE with no acknowledgement. We fetched base.j2 from main and compared it byte-for-byte against the v1.4.39 copy: identical. The vulnerable interpolation is live on the default branch right now. And there is no newer release to upgrade to — v1.4.39 was published on 7 May 2026 and is still the latest, 165 days of no releases, with the three May advisories all timestamped within ninety-eight seconds of each other on the day it shipped.

LangBot: an agent bot that took its instructions from anyone who knew a UUID

CVE-2026-112265 (CVSS 4.0 8.3, CWE-345) is the record on this list with the clearest agent-security consequence. LangBot is an 18,279-star Python chatbot platform that routes messages from Lark/Feishu and other channels into an LLM pipeline with tools. Through v4.11.0 its unified webhook handler parsed and dispatched inbound Lark events with no signature check and no verification-token check. Anyone who knew a bot UUID could POST a fabricated im.message.receive_v1 or card.action.trigger event and drive the pipeline, its tools and its approval flows as another user.

The fix, merged on 18 October 2026, adds _verify_webhook_signature and _verify_webhook_token to both the adapter and the source implementation. Its docstring states the defect better than the CVE does, verbatim:

“Per Lark’s own event-security-verification protocol, a callback is authentic only if its X-Lark-Signature header equals sha256(timestamp + nonce + encrypt_key + body). Without this check any caller who reaches this webhook path can inject fabricated events (spoofed messages, card actions) regardless of whether they hold the app’s Encrypt Key.”

Note what the pre-fix handler did on its first line: data = await request.json. It parsed before it authenticated, because there was nothing to authenticate with. The fix reads the raw body first, verifies, and only then parses — and the new code uses hmac.compare_digest on both paths rather than ==, which is the detail that marks someone who had the whole threat model in view rather than just the missing check. The handler also now fails closed when neither an encrypt key nor a verification token is configured. The reporter, carfeii, filed issue #2627 on 5 October and wrote the fix themselves in PR #2628 — the same reporter whose work we covered in the OneUptime fifteen-advisory batch and the Xinference traversal record this week.

Here is the gap. v4.11.0 was released on 14 October 2026. The fix merged on 18 October. No release has been tagged since. So the CVE’s affected range (“through 4.11.0”) is accurate, the fix is real, the fix is reviewed and merged — and the only way to obtain it is to deploy from main. For a project whose typical installation is a container image pinned to a release tag, “fixed” and “available” are four days and counting apart.

SGLang: three records, three open issues, one unmerged pull request

The three SGLang records are smaller individually and tell the same story structurally.

CVE-2026-112266 (8.2 under CVSS 4.0, 5.9 under 3.1, CWE-187) is the sharpest: a client supplies its own request id, and the abort logic matches rids by prefix. Submit a streaming /generate with a single-hex-character rid, disconnect, and the disconnect handler aborts every in-flight request whose rid begins with that character — other users’ work, cancelled by a one-character collision. The CWE assignment is unusually apt: CWE-187, partial string comparison. This is also not a new report. Issue #41474, filed by a different reporter on 27 September 2026 and titled, verbatim, “/abort_request with a rid also aborts every request whose rid starts wit…”, is still open with one comment, twenty-two days on.

CVE-2026-112267 (6.9) is SSRF in prefill/decode disaggregation mode: the Decode worker’s /generate accepts client-supplied bootstrap_host and bootstrap_port and connects to them, letting an unauthenticated caller probe internal hosts and ports — and, because the connection attempt happens on the shared scheduler loop, stall other inference while it waits. CVE-2026-112268 (5.3, CWE-863) is an authorization bypass in sglang-router: the /ha/* mesh HA management routes skip the control-plane Admin role check, so a holder of the ordinary data-plane API key can read cluster topology, write gossiped configuration, change global rate limits, and call POST /ha/shutdown to dissolve HA coordination. That is the familiar two-plane mistake — a key minted for inference traffic accepted on the routes that manage the fleet.

All three source issues (#45002, #45006, #45008, filed 18 October) are open. The only fix in existence is PR #45017, “[Fix] Require control-plane auth on mesh HA management routes” — four lines added, six removed — and it is open and unmerged. This is the second SGLang entry in this feed today; this morning’s pair covered the expert-distribution recorder kill switch, whose fix PR has been unmerged since 1 October. The pattern in that project is not a shortage of fixes. It is a review queue.

What this batch is actually about

Two threads run through all five, and both are about engineering process rather than about any individual defect.

The first is the sibling path. BentoML sanitised one template and not its twin. The same morning, the ASF published a CVSS 10.0 against Apache Camel for a deserialization filter applied to the component but not to the two instance-bootstrap paths Camel builds itself — also, explicitly, an incomplete fix for an earlier CVE. Camel’s remediation included a test whose comment says the two paths “have to be asserted separately to keep them from drifting apart again.” That is the correct response to this class and it is worth copying verbatim: when you fix an injection or a missing filter, the deliverable is not the patch, it is the enumeration — every call site that renders this value, every constructor that builds this object — plus a test per site so the list cannot silently shrink.

The second is the distance between fixed and installable. A CVE’s “patched version” field is a promise that something exists to upgrade to. Across these five, that promise is empty in every case: no fix at all for BentoML; a merged fix for LangBot with no tag behind it; an unmerged fix for the SGLang router and nothing for the other two. Dependency scanners will light up on all five over the coming days and will not distinguish between “upgrade to 1.4.40” and “there is no 1.4.40.” If your remediation workflow terminates in a version bump, these records have no landing place in it, and the honest status is mitigate or accept, not pending patch.

What to do

BentoML: treat bentofile.yaml and any imported bento archive as untrusted input to a build host, because that is what the CVE says they are. The build host is typically CI, holding registry push credentials and build secrets, so the blast radius is your image supply chain rather than one developer laptop. Concretely: do not run bentoml containerize on bentos you did not author; reject or normalise any docker.env key that is not ^[A-Za-z_][A-Za-z0-9_]*$ before the build, which is a five-line pre-flight check; prefer the envs list form over the docker.env mapping, since the list form is the validated one; and inspect the generated Dockerfile before building if your pipeline allows it. Avoiding docker.base_image keeps you off the legacy template entirely, though for most teams that is the wrong trade.

LangBot: configure an Encrypt Key and a verification token, and understand that on v4.11.0 neither is enforced on the webhook path — so the actual control until a release appears is network: do not expose the unified webhook endpoint to the internet, put it behind a proxy that validates X-Lark-Signature itself, or run from main past commit 43c481d with the usual caveats about unreleased code. Then re-derive what your bot can do: this is an integrity bug (VI:H, VC:L), and the question it asks is what a forged message can cause your pipeline to do — which tools it can reach and which approvals it can satisfy — not what it can read.

SGLang: for the router, stop treating the data-plane API key as a low-privilege credential until #45017 merges, and block /ha/* at the ingress; for the disaggregation SSRF, bound egress from Decode workers to the prefill hosts they legitimately talk to; for the rid collision there is no clean mitigation short of generating rids server-side, so if you accept client rids, require a minimum length and reject short ones at the gateway.

None of the five appears in the CISA Known Exploited Vulnerabilities catalogue, which we checked directly (version 2026.10.16, 1,748 entries, zero hits), and no exploitation has been reported for any of them.

Our verification was read-only. All five CVE identifiers, publication timestamps, vulnStatus values, the disclosure@vulncheck.com source identifier, CVSS 3.1 and 4.0 vectors and scores, CWE assignments, descriptions and references were read first-hand from the NVD 2.0 API. The VulnCheck advisory page for CVE-2026-112264, including the credit to George Chen, was fetched directly, as was the reporter’s write-up at geo-chen/oss, from which the 2 June and 7 June report dates are quoted. Every line of Jinja2 and Python attributed to BentoML — base.j2 lines 45–49, base_v2.j2 lines 70 and 72, the _convert_env list regex and dict branch, the normalize_line definition and the generate_containerfile template-selection branch with its comment — was read at tag v1.4.39 and is quoted verbatim; the byte-for-byte comparison of base.j2 between v1.4.39 and main, and BentoML’s eleven repository security advisories with their titles and timestamps, were measured directly. LangBot issue #2627 (title, author, 5 October creation, closed state) and merge commit 43c481d (18 October, file list, line counts) with the complete diff of both _verify_webhook_signature implementations came from the GitHub API, and the quoted docstring is verbatim from that diff. SGLang issues #41474, #45002, #45006 and #45008 (titles, authors, creation dates, open state, comment counts) and PR #45017 (title, author, open and unmerged, +4/−6) likewise. Release histories and repository metadata for all three projects came from the GitHub API. KEV status was checked directly. Brave Search was used for discovery only and returned automated CVE mirrors with no independent technical analysis of any of the five. We installed nothing, built no container, stood up no model server or bot, sent no webhook, and executed no proof of concept; the BentoML injection string is reproduced only as far as the reporter already published it.

Sources: