Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docxtpl/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,34 @@ def _get_cached_env(autoescape=False):
from .subdoc import Subdoc


# Characters that are illegal in XML 1.0 (Char production, XML spec §2.2).
# These must be stripped from rendered output because a single illegal character
# injected through a context value (e.g. a C0 control like "\x01") makes the whole
# part XML fail strict parsing. The legal XML whitespace controls \t (0x09),
# \n (0x0A) and \r (0x0D) are intentionally preserved.
# This mirrors the well-known openpyxl ILLEGAL_CHARACTERS_RE approach.
_ILLEGAL_XML_CHARS_RE = re.compile(
"["
"\x00-\x08\x0b\x0c\x0e-\x1f" # C0 controls except \t \n \r
"\x7f-\x9f" # DEL + C1 controls (illegal in XML 1.0)
"\ud800-\udfff" # UTF-16 surrogate code points
"\ufdd0-\ufdef" # noncharacters
"\ufffe\uffff" # BMP noncharacters
"]"
)


def _strip_illegal_xml_chars(xml):
"""Remove XML-1.0-illegal characters from a rendered XML string.

Uses a search-guarded fast path so clean input (the common case) is returned
unchanged without allocating a new string.
"""
if _ILLEGAL_XML_CHARS_RE.search(xml):
return _ILLEGAL_XML_CHARS_RE.sub("", xml)
return xml


class DocxTemplate(object):
"""Class for managing docx files as they were jinja2 templates"""

Expand Down Expand Up @@ -474,6 +502,9 @@ def render_xml_part(self, src_xml, part, context, jinja_env=None):
)

raise exc
# Strip XML-1.0-illegal characters injected through context values before
# the rendered string is parsed, so a single bad char can't corrupt output.
dst_xml = _strip_illegal_xml_chars(dst_xml)
dst_xml = self._RE_PARAGRAPH_REMOVE_NEWLINE.sub(r"<w:p\1", dst_xml)
dst_xml = (
dst_xml.replace("{_{", "{{")
Expand Down Expand Up @@ -509,6 +540,7 @@ def render_properties(
initial = getattr(self.docx.core_properties, prop)
template = jinja_env.from_string(initial)
rendered = template.render(context)
rendered = _strip_illegal_xml_chars(rendered)
setattr(self.docx.core_properties, prop, rendered)

def render_footnotes(
Expand Down
26 changes: 26 additions & 0 deletions tests/illegal_xml_chars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
Created : 2026-07-07

@author: Jack Byrne

Checks that XML-1.0-illegal characters injected through context values (such as
the C0 control "\\x01") are stripped from rendered output, so docxtpl never emits
a part XML that fails strict parsing. Illegal chars are placed in the body, an
escaped run, the header, the footer and a core property.
"""

from docxtpl import DocxTemplate

tpl = DocxTemplate("templates/illegal_xml_chars.docx")

context = {
"myvar": "a\x01b",
"escaped": "<c>\x01",
"hdr": "header\x01",
"ftr": "footer\x01",
"prop": "property\x01",
}

tpl.render(context, autoescape=True)
tpl.save("output/illegal_xml_chars.docx")
Binary file added tests/templates/illegal_xml_chars.docx
Binary file not shown.
Loading