Escaping

HTML and Markdown escaping plus safe content handling

4 min read 830 words

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: &lt;script&gt;alert('xss')&lt;/script&gt;

Special characters are replaced:

Character Escaped
< &lt;
> &gt;
& &amp;
" &quot;
' &#39;

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>&lt;script&gt;
result = safe + Markup("<i>")  # <b><i>

# Format escapes arguments
Markup("<p>{}</p>").format("<script>")
# <p>&lt;script&gt;</p>

Escape Function

from kida import Markup

# Escape a string
escaped = Markup.escape("<script>")
# &lt;script&gt;

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 ("&quot;, 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