fix: sandbox chat template rendering to prevent RCE via poisoned models#1475
fix: sandbox chat template rendering to prevent RCE via poisoned models#1475AUTHENSOR wants to merge 1 commit into
Conversation
Chat templates loaded from model files (HuggingFace Hub, local paths) were rendered with an unsandboxed jinja2.Environment, enabling server-side template injection (SSTI). A poisoned model file with a crafted chat_template in its tokenizer_config.json achieves arbitrary code execution when loaded by guidance. HuggingFace's transformers uses ImmutableSandboxedEnvironment for the same operation precisely because chat templates from model files are untrusted. Guidance extracted the template string and re-rendered it outside HF's sandbox, bypassing that protection. Fix: replace Environment with ImmutableSandboxedEnvironment (the same class HuggingFace uses). 5 tests: blocks __init__ chain SSTI, blocks __class__ chain SSTI, renders normal templates, renders tojson filter, renders Llama3-style template. Signed-off-by: John Kearney <johndanielkearney@gmail.com>
866f0af to
faffde5
Compare
ErenAta16
left a comment
There was a problem hiding this comment.
Verified this independently rather than taking the vulnerability claim on faith. Ran the exact repro from the PR description locally:
from jinja2 import Environment, BaseLoader
malicious_template = "{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}"
Environment(loader=BaseLoader).from_string(malicious_template).render(messages=[], tools=None, add_generation_prompt=True)
# -> real shell output, e.g. "uid=1001(...) gid=1001(...) groups=..."Confirmed. Then confirmed the fix actually closes it:
from jinja2.sandbox import ImmutableSandboxedEnvironment
ImmutableSandboxedEnvironment(loader=BaseLoader).from_string(malicious_template).render(...)
# -> jinja2.exceptions.SecurityError: access to attribute '__init__' of 'TemplateReference' object is unsafe.Also checked whether this is the only unsandboxed rendering path in the codebase before trusting the "only one call site" framing: grepped for Environment( and jinja2 imports repo-wide, and _engine.py:676 is the only real usage (the other hit, _tokenizer.py, is just a comment mentioning jinja2, no actual Environment call). So this one-line fix does close the whole hole, not just one instance of a wider pattern.
The new test file is solid too: it doesn't just check that the malicious payload raises something, it also has three tests confirming normal chat templates (loops, conditionals, tojson, Llama-3-style headers) still render correctly under the sandbox, so this isn't trading RCE for broken legitimate templates.
This is a real, serious fix (server-side template injection -> RCE from a poisoned model file's tokenizer_config.json is about as bad as it gets for a library that loads models from arbitrary sources). Looks correct and complete.
fix: sandbox chat template rendering to prevent RCE via poisoned models
Summary
Chat templates loaded from model files (HuggingFace Hub, local paths) were
rendered with an unsandboxed
jinja2.Environment, enabling server-sidetemplate injection (SSTI). A poisoned model file with a crafted
chat_templatein itstokenizer_config.jsonachieves arbitrary codeexecution when loaded by guidance.
HuggingFace's
transformerslibrary usesImmutableSandboxedEnvironmentforthe same operation precisely because chat templates from model files are
untrusted. Guidance extracted the template string and re-rendered it outside
HF's sandbox, bypassing that protection.
Fix: Replace
EnvironmentwithImmutableSandboxedEnvironment(one-linechange). This is the same class HuggingFace uses for chat template rendering.
The vulnerability
Environment(not sandboxed) allows full access to Python internals viaJinja's template object chain. A malicious template executes arbitrary Python.
Guidance does not import or use any Jinja sandbox anywhere in the codebase
(confirmed:
grep -rn "SandboxedEnvironment" guidance/returns empty).Reproduction
After the fix, the same template raises
SecurityError: access to attribute '__init__' of 'TemplateReference' object is unsafe.Supply-chain attack scenario
tokenizer_config.jsoncontaining a maliciouschat_templatefieldmodel = guidance.models.Transformers("attacker/model")via the unsandboxed
EnvironmentThe victim does nothing wrong — loading a model from HuggingFace Hub is the
normal usage pattern.
Why this bypasses HuggingFace's protection
HuggingFace's
transformersrenders chat templates viaapply_chat_template(),which uses
ImmutableSandboxedEnvironmentinternally. Guidance does NOT callapply_chat_template(). Instead, it extracts the template string(
self.get_chat_template().template_str, sourced fromhf_tokenizer.chat_templateat_transformers.py:85) and re-renders it withits own unsandboxed
Environment. This re-rendering bypasses the sandbox thatHuggingFace applies to the same template.
The fix
One-line change.
ImmutableSandboxedEnvironmentis the same class HuggingFaceuses. It blocks attribute access to Python internals (
__init__,__globals__,__builtins__,__class__) while rendering normal templates correctly.Tests
5 tests in
tests/unit/test_chat_template_sandbox.py:test_sandbox_blocks_attribute_access_ssti: the__init__.__globals__chain that achieves RCE is blocked
test_sandbox_blocks_class_attribute_access: the__class__.__mro__chain is blocked
test_sandbox_renders_normal_chat_template: normal templates (loops,conditionals, variable access) render correctly (no false positive)
test_sandbox_renders_template_with_tojson_filter: thetojsonfilter(used in real chat templates for tool definitions) works under the sandbox
test_sandbox_renders_llama3_style_template: a Llama 3-style templatewith bos_token, header tokens, and trim filter renders correctly
No regression: 30 existing unit tests pass unchanged.
Impact
machine that loads it via guidance and sends a chat message
Scope
Two files:
guidance/models/_engine/_engine.py(the fix) andtests/unit/test_chat_template_sandbox.py(new test file). Diff: +84/−2.