Skip to content

Template escaping — autoescape is OFF

Every plugin that renders user-supplied text must escape it itself

FastPluggy builds its Jinja environment without autoescaping. Nothing in the framework, your tests, or the linters will tell you when a template interpolates unescaped user input — the page simply renders, and the injection works.

What the host actually does

fastpluggy.py constructs the shared environment as:

jinja_env = Environment(loader=loaders)  # nosec B701

Jinja2's Environment defaults to autoescape=False, and no autoescape= argument is passed. The # nosec B701 comment suppresses bandit's warning for exactly this — so the one tool that flags it by default is silenced fleet-wide. The same pattern appears in the error and degraded-mode handlers.

This is a property of the host, not of your plugin. It applies to every plugin loaded into the app, and a plugin author cannot opt in per-plugin by changing a local setting.

What you must do

Filter every interpolation of a value that a human could have typed:

{{ user_supplied | e }}

"Human could have typed" is broader than a form field — it includes anything read back from the database, a filename, a webhook payload, an LLM response, or a captured page title.

Why a review pass is not enough

A missing | e looks exactly like a present one until someone submits a payload. There is no failing test, no lint error and no runtime warning, so the defect survives review by being invisible rather than by being argued for.

Add a structural test that walks your plugin's own templates and asserts every interpolation of a known-tainted variable carries a filter. It runs in CI, it covers templates added later, and it does not depend on anyone remembering the rule.

Escaping is not the only control

| e handles HTML context. A value interpolated into an inline <script>, a style attribute, or a URL needs the escaping appropriate to that context — HTML-escaping a string inside a <script> block does not make it safe.