Importing a career file: the DOCTYPE refusal reads only the first 4 KB as ASCII, so a later or UTF-16 DTD is parsed #378
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#378
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, reading the importer kind's refusals and the Europass XML reader, and settled by feeding crafted documents toreader.read.Related to #105.
Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atplugins/europass/reader.py:281-293(read_xml, withfromstringat line 293) and its callers of the refusal at:156and:240;plugins/base.py:363-364andresume/views.py:442have not moved. The same three documents give the same three results.refuse_unreadableis the only thing standing between an uploaded career file and the standard library's XML parser, and it looks for the bytes<!DOCTYPEonly in the first 4096 bytes and only in an ASCII-compatible encoding. XML allows any amount of comment or processing instruction before the DOCTYPE, and allows UTF-16. Either way the declaration is not seen, the file is accepted, andread_xmlparses it withxml.etree.ElementTree.fromstring, which expands internal entities. The guarantee the code, its comment and the wiki all state ("refused outright, without being parsed") does not hold.plugins/base.py:363-364(refuse_unreadable:head = data[:4096]...andre.search(rb"<!DOCTYPE", data[:4096], re.I));plugins/europass/reader.py:404-416(read_xml, whose docstring says the refusal "is what makesfromstringbelow safe to call", andElementTree.fromstring(data) # noqa: S314 - no DOCTYPE); the callersresume/views.py:442,plugins/europass/reader.py:241and:325(the XML attached to a Europass PDF). The wiki pageYour-career-record.md:285-287promises the refusal.SkillsPassportdocument that declares an internal entity and uses it inFirstName:ImportRefused, as intended;person["first_name"]comes back as the entity's expansion;_read_documentsends it toread_xmlbecause its first byte is<).plugins/api.pyexportsrefuse_unreadableto third-party importers as the kind's guarantee;Writing-a-plugin.md:1212-1218does tell their authors to use a parser that resolves no entities, but the core's own reader does not follow that advice.defusedxmlis already a dependency andcore/pictures.py:41parses SVG with it, so the Europass reader is the one XML parser in the core that does not.read_xmlwithdefusedxml.ElementTree.fromstring(its defaults forbid a DTD's entities and external references; passforbid_dtd=Trueto refuse any DOCTYPE outright) and mapdefusedxml's exceptions to the existing refusal sentence. Makerefuse_unreadabledecide with a parser rather than a byte search: feed XML-looking data to an expat parser whoseStartDoctypeDeclHandlerraises, or detect the encoding first, so neither position nor encoding matters. Drop the "safe because of the refusal" wording in the docstring, and keep the wiki sentence only once it is true.Done when
tests/test_importers.pyrefuses a document whose DOCTYPE follows a comment longer than 4 KB, and the same document in UTF-16LE and UTF-16BE, with and without a byte-order mark.tests/test_europass.pyrefuses each of those inside a PDF attachment as well, andread_xmlcalled directly on such a document raises rather than expanding the entity.