Passing HTML to template using Flask/Jinja2
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Passing HTML into a Flask template is easy mechanically, but the security implications matter more than the syntax. Jinja2 escapes variables by default, so the real question is not just how to render HTML, but when a value is trusted enough to be rendered as markup instead of plain text.
Why Jinja2 Escapes HTML by Default
In a normal Flask template, this expression is escaped:
If body contains "<strong>Hello</strong>", the browser will display the tags as text instead of rendering bold output. That behavior is intentional. Autoescaping protects your app from cross-site scripting, which is exactly what happens when untrusted content is injected into a page as live HTML.
A small Flask example shows the default behavior:
Visit the route and you will see the literal markup characters, not bold text. That is usually what you want for raw user input.
Rendering Trusted HTML Intentionally
If the HTML was generated by your own application and you know it is safe, you can mark it as trusted. The most direct approach is Markup from markupsafe.
Template:
Because bio is marked safe, Jinja2 will render it as HTML rather than escaping it.
You can also use the safe filter in the template:
That works, but in many codebases it is better to make the trust decision in Python rather than sprinkling |safe through templates. It is easier to review and easier to test.
Sanitizing User-Provided HTML
If the content comes from users, a database field, or a CMS, “safe” and “works” are not the same thing. Untrusted HTML should be sanitized before rendering.
A common option is bleach:
This keeps allowed formatting while stripping dangerous tags and attributes. That is a much safer pattern than marking user data as safe directly.
Passing Structured Data Is Often Better
A lot of applications do not need to pass HTML at all. If the content is really data, pass the data and let the template decide how to render it.
For example, instead of assembling an HTML list in Python:
pass structured values:
Template:
This is safer, easier to maintain, and keeps presentation logic where it belongs.
When Markup Is Appropriate
Markup is appropriate when your code generates the HTML itself or when you have sanitized the content already. It is not a license to bypass escaping because “the string looks harmless.”
A good rule is:
- trusted application-generated markup can be wrapped in
Markup - user-generated markup should be sanitized first
- plain text should be left to autoescape normally
That rule keeps the default safe behavior intact while still allowing rich content when the data model really needs it.
Common Pitfalls
The biggest mistake is using |safe on raw user input. That turns a formatting problem into a security bug.
Another common problem is building large HTML fragments in Python when the data should have been passed as structured values and rendered in Jinja2.
Developers also sometimes assume that “internal” data is automatically safe. If the content ultimately came from an editor, import job, or database field, it still needs a trust decision.
Finally, make the safe-or-unsafe choice in one place. Mixing Markup in some views and |safe in some templates makes audits harder.
Summary
- Jinja2 escapes variables by default to prevent cross-site scripting.
- Use
Markupor|safeonly for content you trust or have sanitized. - Sanitize user-provided HTML before rendering it as markup.
- Prefer passing structured data to templates instead of assembling HTML in Python.
- Treat HTML rendering as both a template concern and a security decision.

