From db7379fb2e2d46b355313603f8bb9ffbdecf5c78 Mon Sep 17 00:00:00 2001 From: Jack Byrne <46843566+JackByrne@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:25:00 +0100 Subject: [PATCH 1/2] Strip illegal XML chars from rendered output Add XML 1.0 illegal-character sanitization to template rendering and core property rendering so control/noncharacter code points injected via context values no longer break DOCX XML parsing. The change uses a regex-based fast path and preserves valid XML whitespace controls. Also adds a regression test template/script covering body text, escaped content, header/footer, and core properties. --- docxtpl/template.py | 31 +++++++++++++++++++++++++ tests/illegal_xml_chars.py | 26 +++++++++++++++++++++ tests/templates/illegal_xml_chars.docx | Bin 0 -> 17173 bytes 3 files changed, 57 insertions(+) create mode 100644 tests/illegal_xml_chars.py create mode 100644 tests/templates/illegal_xml_chars.docx diff --git a/docxtpl/template.py b/docxtpl/template.py index cf339df..6e03bc7 100644 --- a/docxtpl/template.py +++ b/docxtpl/template.py @@ -69,6 +69,33 @@ 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-\x84\x86-\x9f" # DEL + C1 controls (illegal in XML 1.0) + "\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""" @@ -474,6 +501,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"\x01", + "hdr": "header\x01", + "ftr": "footer\x01", + "prop": "property\x01", +} + +tpl.render(context, autoescape=True) +tpl.save("output/illegal_xml_chars.docx") diff --git a/tests/templates/illegal_xml_chars.docx b/tests/templates/illegal_xml_chars.docx new file mode 100644 index 0000000000000000000000000000000000000000..7285bcad29ac68a0dd6345e5a98c1ecf9aa3ccd5 GIT binary patch literal 17173 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 Date: Tue, 7 Jul 2026 11:33:58 +0100 Subject: [PATCH 2/2] Expand XML illegal character filtering Update the illegal XML character regex to better match XML 1.0 constraints by covering the full DEL/C1 control range (`\x7f-\x9f`) and excluding UTF-16 surrogate code points (`\ud800-\udfff`). This prevents invalid characters from leaking into generated document XML. --- docxtpl/template.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docxtpl/template.py b/docxtpl/template.py index 6e03bc7..385ab05 100644 --- a/docxtpl/template.py +++ b/docxtpl/template.py @@ -78,7 +78,8 @@ def _get_cached_env(autoescape=False): _ILLEGAL_XML_CHARS_RE = re.compile( "[" "\x00-\x08\x0b\x0c\x0e-\x1f" # C0 controls except \t \n \r - "\x7f-\x84\x86-\x9f" # DEL + C1 controls (illegal in XML 1.0) + "\x7f-\x9f" # DEL + C1 controls (illegal in XML 1.0) + "\ud800-\udfff" # UTF-16 surrogate code points "\ufdd0-\ufdef" # noncharacters "\ufffe\uffff" # BMP noncharacters "]"