Kida auto-escapes output by default. HTML mode protects browser output from injection, Markdown mode protects CI reports and comments from accidental formatting, and terminal mode sanitizes ANSI control sequences.
Autoescape
Withautoescape=True(default), output is HTML-escaped:
env = Environment(autoescape=True)
template = env.from_string("{{ content }}")
html = template.render(content="<script>alert('xss')</script>")
# Output: <script>alert('xss')</script>
Special characters are replaced:
| Character | Escaped |
|---|---|
< |
< |
> |
> |
& |
& |
" |
" |
' |
' |
Disable Autoescape
For specific templates:
# Callable autoescape
def should_escape(template_name):
if template_name is None:
return True
return template_name.endswith(".html")
env = Environment(autoescape=should_escape)
Globally (not recommended):
env = Environment(autoescape=False)
Safe Filter
Mark content as trusted for the active render surface:
{{ html_content | safe }}
With optional reason for code review:
{{ cms_block | safe(reason="sanitized by bleach library") }}
{{ admin_html | safe(reason="admin-only content") }}
Under autoescape=True / autoescape="html", safe means trusted HTML. Under autoescape="markdown", safemeans trusted Markdown. Do not use it for untrusted issue text, PR bodies, tool output, user comments, or any value that has not already crossed a clear trust boundary.
Markdown Autoescape
Use Markdown mode for GitHub step summaries, PR comments, release notes, and other Markdown output:
from kida.markdown import markdown_env
env = markdown_env()
template = env.from_string("{{ text }}")
template.render(text="Use *literal* [text]")
# Use \*literal\* \[text\]
Markdown autoescape targets CommonMark/GFM formatting triggers. It escapes inline backslashes, backticks, emphasis markers, brackets, and angle brackets. Since 0.9.0, it no longer escapes punctuation that is harmless in normal inline text, such as hyphens, parentheses, hashes, pipes, and tildes. Block-leading Markdown markers like #, >, -, +, and ordered-list digits are still escaped when they appear at the start of a line.
Markup implements both __html__ and __markdown__, so safeis honored by Markdown autoescape:
from kida.markdown import markdown_env
env = markdown_env()
template = env.from_string("{{ body }}\n{{ body | safe }}")
template.render(body="## Trusted heading")
# \## Trusted heading
# ## Trusted heading
If you keep committed snapshots for Markdown reports, regenerate them after upgrading to 0.9.0. Expected diffs usually remove unnecessary backslashes from prose, dates, diagnostic codes, and function-call text.
Markup Class
Create safe content in Python:
from kida import Markup
# String marked as safe
safe_html = Markup("<b>Bold</b>")
template.render(content=safe_html) # Not escaped
Markup preserves safety in HTML and Markdown modes. Treat it like safe: only wrap content that is already sanitized or authored by trusted code.
Markup Operations
# Concatenation escapes unsafe strings
safe = Markup("<b>")
result = safe + "<script>" # <b><script>
result = safe + Markup("<i>") # <b><i>
# Format escapes arguments
Markup("<p>{}</p>").format("<script>")
# <p><script></p>
Escape Function
from kida import Markup
# Escape a string
escaped = Markup.escape("<script>")
# <script>
Common Patterns
Pre-Sanitized Content
When content is already sanitized:
import bleach
cleaned = bleach.clean(user_html, tags=["b", "i", "a"])
template.render(content=Markup(cleaned))
Rendered Markdown
import markdown
html = markdown.markdown(source)
template.render(content=Markup(html))
HTML in JSON
Thetojson filter outputs JSON marked safe for the template engine. For trusted data, embedding that JSON inside a classic <script>block is often convenient:
<script>
const data = {{ user_data | tojson }};
</script>
Untrusted or attacker-controlled data can contain </script> (or <) and close the script element early, which breaks parsing and can enable XSS. Prefer <script type="application/json"> plus JSON.parse on textContent, or serve JSON from an endpoint, instead of inlining raw tojsonoutput in executable script for untrusted content.
JSON in HTML attributes
Double-quoted attributes must not contain raw" in the value. Default tojsonoutput includes double quotes, so this breaks the attribute:
{# Broken — quotes terminate the attribute early #}
<div x-data="setup({{ config | tojson }})">
Use tojson(attr=true) so the JSON is HTML-entity-encoded (" → ", etc.). The browser decodes entities before JavaScript reads the attribute (e.g. Alpine x-data):
<div x-data="{{ config | tojson(attr=true) }}">
Alternatively, keep default tojson and wrap the attribute in single quotes, or put JSON in <script type="application/json">and read it from script.
Security Best Practices
Always Use Autoescape
# ✅ Autoescape on (default)
env = Environment(autoescape=True)
# ❌ Never disable globally
env = Environment(autoescape=False)
Audit safeUsage
{# ✅ Document why it's safe #}
{{ content | safe(reason="sanitized by bleach") }}
{# ❌ Unmarked safe usage #}
{{ user_input | safe }}
Validate Content
# ✅ Sanitize before marking safe
cleaned = bleach.clean(content, tags=ALLOWED_TAGS)
Markup(cleaned)
# ❌ Never mark user input safe directly
Markup(request.form["content"]) # XSS vulnerability!
Escape Filter
Explicitly escape content:
{{ content | escape }}
{{ content | e }} {# Short alias #}
Useful when autoescape is disabled for a template.
See Also
- Filter Reference — escape, safe, striptags
- Variables — Output expressions
- Error Handling — Debug template issues