Skip to content

test: lock in chat-template (minijinja) RCE-safety with regression tests #128

@inureyes

Description

@inureyes

Background

CVE-2026-5760 (CVSS 9.8) showed that an LLM serving framework which renders a model-supplied chat_template through an unsandboxed template engine can be driven to remote code execution: a malicious model embeds a Server-Side Template Injection (SSTI) payload in tokenizer.chat_template, and when the template is rendered the payload reaches interpreter internals (e.g. Python's __class____subclasses__os.system).

mlxcel renders model-supplied chat templates in src/server/chat_template.rs via minijinja. By construction this is not vulnerable to that RCE class:

  • minijinja::Value is a sealed type system with no reflection attributes (__class__ / __mro__ / __subclasses__ / __globals__), so there is no gadget chain from a string to an arbitrary callable.
  • configure_environment() (src/server/chat_template.rs:586) registers only pure helpers (raise_exception, strftime_now with its format argument ignored, and a set_unknown_method_callback at line ~616 that implements pure string/dict methods). None perform process, filesystem, or network I/O.
  • No template loader is configured (no {% include %} file disclosure), and pycompat / minijinja-contrib are not enabled.
  • Clients cannot supply template source over HTTP — only model files and the operator --chat-template flag can.

This issue does NOT report a vulnerability. It adds regression tests so this safe posture is enforced going forward and a future change (e.g. enabling pycompat, adding a file loader, or registering an I/O-capable function) cannot silently reintroduce the risk.

Tasks

  • Add tests (in the #[cfg(test)] module of src/server/chat_template.rs) that render representative Jinja2 SSTI gadget payloads through ChatTemplateProcessor and assert each produces either a render error or inert literal output — never a side effect. Cover at least:
    • Python reflection chains: {{ ''.__class__ }}, {{ ().__class__.__bases__ }}, {{ ''.__class__.__mro__[1].__subclasses__() }}, {{ self.__init__.__globals__ }}.
    • Builtin/global escape attempts: {{ config }}, {{ lipsum }}, {{ get_flashed_messages() }}, {{ cycler.__init__.__globals__ }}.
  • Add a test proving data is not second-order rendered: a message content (and a chat_template_kwargs value) of {{ 7 * 7 }} / {{ ''.__class__ }} must appear verbatim in the output, proving request-controlled data is never evaluated as template source.
  • Add a guard test that fails if configure_environment ever registers a function/global outside an explicit allowlist, or if a template loader is ever set — so future additions are caught at test time.
  • Tests must run in default CI: use inline template strings like the existing unit tests; do NOT depend on a local models/ directory (i.e. not the #[ignore]d test_all_local_model_templates_render).

Acceptance criteria

  • New tests pass and run as part of the default cargo test (not #[ignore]d).
  • A deliberately-introduced regression — e.g. registering a dangerous function in configure_environment — makes at least one new test fail.
  • No runtime behavior change; tests only.

References

  • CVE-2026-5760 / CERT VU#915947 (SGLang chat-template SSTI → RCE)
  • src/server/chat_template.rsconfigure_environment (~line 586), set_unknown_method_callback (~line 616), apply / apply_raw_with_kwargs (~line 347), from_model_path (~line 64)

Companion hardening issue: #129

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions