Last reviewed: November 1, 2026 (Asia/Shanghai).
Direct answer
Prevent server-side template injection in coding-agent patches by keeping template source under the application’s control and passing external text only as render data. In Jinja, loading a reviewed template and supplying message=external_text preserves that distinction. Concatenating the same text into source passed to Environment.from_string(), Template(), or Flask’s render_template_string() changes it into instructions the server evaluates.
The Jinja API reference
explicitly distinguishes loading templates, compiling source strings, and passing context to render(). Review the origin of the source argument, including stored and generated text, rather than banning an API name without examining its use.
HTML autoescaping does not enforce this source boundary. Flask’s Templates documentation
says render_template_string() enables autoescaping by default; the supplied string is still a template. If users genuinely need to author templates, give that feature a separate restricted rendering design. The Jinja sandbox documentation
supports that use while warning that sandboxing alone does not provide complete security or resource limits.
Who this is for
Use this guide when reviewing an agent’s changes to notification previews, email generation, report builders, document exports, localization helpers, or administrative template editors. It focuses on Jinja and Flask; another engine needs its own documented source, context, and sandbox semantics.
Trace request fields, imported documents, repository files, database values, and model responses. A string does not become trusted template source because the application stored it or an agent generated it. Review background delivery and export jobs alongside the interactive preview: they often render the same content through different helpers.
Key takeaways
- Keep template structure in reviewed application assets; pass changing content through render context.
- Map permitted layout choices to fixed template names instead of accepting arbitrary names or paths.
- Follow the complete rendering pipeline. A later compilation can turn previously harmless output into source.
- Inspect Flask context processors, Jinja globals, filters, and callable objects for authority exposed to templates.
- Treat user-authored templates as a separate capability with a sandbox, minimal data, and externally enforced resource limits.
- Test literal output and side effects through the real render path, including saved previews and delivery jobs.
Sources checked
Three distinct primary documentation pages returned HTTP 200 at their canonical URLs on November 1, 2026:
- Pallets: Jinja API
documents loaders,
get_template(),from_string(), render context, autoescaping, undefined values, and shared globals. - Pallets: Jinja Sandbox documents restricted attribute and callable access, immutable sandbox behavior, rendering errors, context minimization, and CPU, memory, and output risks.
- Pallets: Flask Templates documents automatic escaping, standard context, context processors, filters, and streaming integration.
All three identify the 3.1.x documentation series. These maintained official references support an evergreen code-review task; their retrieval date is not a new vulnerability announcement. The example, acceptance matrix, and worker policy below are engineering recommendations based on those semantics. No repository-specific test or deployment outcome is claimed.
Contract details to verify
Label source, template selection, and data separately
For every changed rendering call, record three inputs: who controls the template’s bytes, who chooses the template, and which values are supplied as data. A trusted filename does not establish trusted bytes if the loader reads from a user-writable upload directory. Conversely, an ordinary string containing template punctuation can remain harmless when it is only a context value.
| Input | Intended authority | Review condition |
|---|---|---|
| Template source | Reviewed application structure | Supplied by deployment-owned assets or code, without external concatenation |
| Layout token | One permitted presentation choice | A closed mapping selects a fixed template; authorization applies where choices differ by user |
| Message text | Display data | Passed as a bounded plain string through context |
| Render helpers | Explicit application capabilities | Globals, filters, tests, and callables expose only required behavior |
| Rendered output | Content for a declared destination | Never compiled again; HTML, email, or other destination rules remain explicit |
Search the changed code and reachable wrappers for from_string, Template, render_template_string, stream_template_string, dynamic loaders, and string construction near rendering. Examine the argument’s origin. A code-owned constant passed to a string-template API has a different trust contract from a submitted message passed to that API.
Keep an ordinary notification on a fixed template
This illustrative Flask helper assumes the caller has already authorized the notification operation. Both the mapping and templates/notices/standard.html are application-owned. The size limit is an example product choice, measured in Python string characters; enforce request-byte limits separately before decoding.
from flask import render_template
NOTICE_TEMPLATES = {"standard": "notices/standard.html"}
def render_notice(layout, message):
if type(layout) is not str or layout not in NOTICE_TEMPLATES:
raise ValueError("notice_layout_invalid")
if type(message) is not str or len(message) > 4000:
raise ValueError("notice_message_invalid")
return render_template(NOTICE_TEMPLATES[layout], message=message)
The complete template is deliberately small:
<section class="notice">
<h2>Notification</h2>
<p>{{ message }}</p>
</section>
For a message containing {{ 6 * 7 }}, the paragraph should display those literal characters. The trusted template evaluates its message placeholder once; it does not recursively interpret the value as another template. A helper that subsequently feeds the rendered page to render_template_string() would break this contract.
Keep message a plain string at this boundary. Do not wrap external values in Markup, apply |safe, or disable escaping to repair a display issue. Those changes affect HTML handling and require a separate XSS rendering review
. This example inserts text into an ordinary HTML element; it does not establish safe interpolation into JavaScript, CSS, URLs, or email headers.
Inspect the context and every later pass
Flask documents default access to config, request-bound objects such as request, session, and g, and helper functions. Context processors can add more values and functions. A compromised source boundary may therefore expose much more than the one message passed explicitly by the helper. Inventory those additions, custom filters, extensions, and objects with methods before approving dynamic rendering.
Prefer small data records over database sessions, service clients, request objects, or objects whose properties and methods perform I/O. For separate user-template rendering, construct a dedicated environment rather than assuming the ordinary Flask environment has a narrow context.
Jinja also documents that environment and template globals can be shared and should not be changed after templates load. Supply request-specific values to render() instead of updating shared globals for each request. Review caches and helpers for cross-request state even when the template source is fixed.
Draw the pipeline through preview, save, queue, delivery, and export. Mark every compilation step. Escaping, sanitizing, storing, or rendering a value once does not authorize a later component to compile the result as template source. Fix that later boundary rather than adding a punctuation filter to the original input.
Give user-authored templates their own capability boundary
If a reporting feature intentionally accepts Jinja source, document its allowed variables, operations, output format, and resource budget. Use a supported, patched Jinja release and a separately configured sandbox environment. Remove unnecessary globals, filters, tests, extensions, and callable objects; pass only the report’s minimal data.
The sandbox reference says callables are considered safe by default unless marked unsafe. An ordinary permitted function can still have harmful side effects. Likewise, ImmutableSandboxedEnvironment restricts modifications to supported built-in mutable containers; it does not make arbitrary application objects inert. Review helper behavior directly instead of relying on either class name as a complete policy.
Enforce input bytes, CPU, memory, wall time, and output bytes outside the template’s cooperation. A short template can produce large output, so checking length only after render() returns cannot prevent that allocation. A proposed worker should render within enforced limits, keep output bounded, and release a result only after successful completion. On deadline or limit failure, terminate and reap the worker and discard partial output.
Jinja notes that it renders text without understanding downstream JavaScript or other languages. A template sandbox therefore does not approve its output for every destination. Define the HTML or other output contract separately. If output is later passed to an external converter, include the site’s process-launch review .
Happy-path operator workflow
- Freeze the patch revision and list all changed render calls, loaders, wrappers, and downstream consumers.
- Label template bytes, layout selection, and context values by origin and intended authority.
- For ordinary messages, move changing text into context and keep the template and layout mapping application-owned.
- Inspect the effective environment, including context processors, globals, filters, extensions, and shared state.
- Exercise normal preview, save, reload, background delivery, and export paths with synthetic content.
- Run the acceptance matrix against the actual configured renderer, then retain sanitized assertions tied to the revision and dependency lock.
Record the template identifier or digest, engine and framework versions, context field names, fixture IDs, output assertions, controlled error class, and any worker cleanup result. Keep raw messages, configuration values, credentials, and full exception dumps out of general CI evidence.
Prove the boundary with an acceptance matrix
For the fixed notification example, assert the paragraph’s decoded text and inspect the final HTML structure. A response containing 42 somewhere else is not evidence that the message was evaluated; compare the exact target field.
| Case | Required observation |
|---|---|
| Ordinary message, Unicode, and an empty message | Exact intended text under the stated product contract |
Message {{ 6 * 7 }} | Literal expression remains in the paragraph; it is not replaced by 42 |
Message containing {% if true %}sample{% endif %} | Statement syntax remains literal data |
| Synthetic configuration lookup supplied as message text | Literal text remains; the test-only configuration canary is absent |
| Message containing HTML tags and quotes | Text survives appropriately escaped; no injected elements appear |
| Unknown layout token or wrong input type | Controlled rejection before template selection or rendering |
| Message at 4000 characters and at 4001 characters | Boundary accepted, excess rejected; separate transport-byte tests also pass |
| Saved content reopened and delivered by a worker | The same literal-data contract holds through every path |
| Missing trusted template or render exception | Controlled failure; no retry using message text as source |
| Separate user-template feature exceeds a resource limit | Worker stops, partial output is discarded, and cleanup is observed |
| Separate user-template feature tries a forbidden capability | Policy-defined denial with no filesystem, network, or application mutation |
Use only test-owned configuration values and inert recording adapters. For user-template tests, specify the allowed syntax and exact denial behavior before selecting fixtures; ordinary fixed-template messages and intentionally executable templates have different acceptance rules. Never use production secrets or an exploit chain as a probe.
Prove the harness can distinguish source from data with a test-only positive control: compile the constant expression {{ 6 * 7 }} in a disposable engine instance and observe 42. Keep that control outside application routes. Then send the expression through the real notification path and require literal text. This checks that the fixture is meaningful without exposing a vulnerable endpoint.
These are proposed tests. The helper and matrix have not been executed against an application in this article.
Error-path operator workflow
If external text is evaluated, block the patch and retain the smallest synthetic reproduction for that revision. Find the first point where data becomes source, repair it, and inspect sibling preview and delivery paths. Recreate clean fixtures and repeat both ordinary and adversarial cases after the change.
On invalid input, reject before rendering. On a missing asset, syntax error, undefined value, sandbox denial, or worker limit, return a controlled application failure. Do not retry through an unsandboxed environment, compile the original message as a fallback, or emit partially rendered content. Catch rendering failures at the job or request boundary without exposing raw exceptions to readers.
StrictUndefined can make missing variables fail visibly; it does not restrict what valid template source can do. Similarly, a queue retry must retain the same rendering policy. A transient failure is not authorization to widen context or disable a sandbox.
Failure modes
Autoescaping is presented as the injection fix. It handles output characters in its supported context. Remove untrusted source construction separately.
A trusted template name loads untrusted bytes. Review loader roots, write permissions, override directories, and database-backed template stores.
Only the first render is tested. Exercise stored previews, queued delivery, and exports to expose later compilation.
A sandbox receives powerful helpers. Remove side-effecting objects and narrow callable policy; the default does not reject every function call.
An output cap is checked after full rendering. Enforce worker memory and runtime limits during execution as well as a bounded output policy.
A delimiter denylist replaces the source boundary. Keep external values in context regardless of punctuation; supported syntax and configuration can change.
FAQ
Is every use of render_template_string unsafe?
No. The source’s origin matters. A fixed, reviewed source string can receive untrusted values as context. Compiling a submitted or concatenated message gives that message template authority.
Does passing a string to render evaluate its template expressions?
An ordinary context string is data for that render. It becomes source only if application code later sends it, or output containing it, through another compilation path.
Can an authenticated administrator edit templates safely?
Authentication identifies the editor; it does not define which server capabilities their template may use. Choose explicitly between a deployment-level trusted author and a restricted user-template feature, then enforce and test that authority.
Does a sandbox also prevent browser XSS?
No. Server capability restrictions and browser output safety need separate contracts. Review the final destination, escaping, and any allowed rich HTML alongside the sandbox policy.
Reader next step
Choose one agent-written rendering change. Mark its template source, layout selector, context, and every later compilation step. Move ordinary external text into context, add the literal-expression and HTML fixtures, and verify saved and background paths. Approve the revision when the intended content still renders, external data never gains template authority, and failures preserve the same policy.