Repository navigation
Conversation
vCalAddress.to_xcal() wrote the value into <cal-address> unfiltered, so a C0 control character from jCal produced XML that is not well-formed. Route it through _to_valid_xml_string like vText, vUri and vUnknown, and add U+FFFE and U+FFFF to that filter. AI use: Claude Opus 5.5 (claude-opus-5-5) via Claude Code. I asked it to look for a security-relevant bug in icalendar; it found the vCalAddress gap noted in PyCalendar#1888, wrote a jCal -> xCal reproducer, the fix, and the test changes, and ran the test suite and ruff. I reviewed the output.
|
Thanks for the pull request, @alhudz! A maintainer will review it shortly. |
|
This pull request did not pass quality checks and AI use is suspected. Please review Contribute and make any necessary amendments. |
|
@faresrafat3 It is |
niccokunzmann
left a comment
There was a problem hiding this comment.
Thanks for the PR. I have some questions.
|
|
||
|
|
||
| _VALID_XML_CHARACTERS = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f]") | ||
| _VALID_XML_CHARACTERS = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\ufffe\uffff]") |
There was a problem hiding this comment.
I think, this needs a docstring saying why which characters are allowed.
With references to the source.
There was a problem hiding this comment.
Added in dab0b71. The docstring quotes the Char production from XML 1.0 section 2.2 and lists what the pattern matches: C0 controls apart from tab, LF and CR, plus #xFFFE and #xFFFF.
It also notes that lone surrogates are outside Char but are not matched. to_ical() rejects them, but Event with SUMMARY "a\ud800" currently gives <text>a�</text> from to_xcal(), which isn't well-formed either. I can add \ud800-\udfff to the pattern here if you want it covered in this PR, or leave it for a separate one.
niccokunzmann
left a comment
There was a problem hiding this comment.
Technically fine. Let's have another docs review.
|
#1966 added @niccokunzmann the push dismissed your approval. |
stevepiercy
left a comment
There was a problem hiding this comment.
Thanks for your contribution, @alhuda! I've made a few suggestions. Would you please take care? Thank you!
| @@ -0,0 +1 @@ | |||
| Fixed :meth:`vCalAddress.to_xcal() <icalendar.prop.cal_address.vCalAddress.to_xcal>` writing characters that are not allowed in XML, such as a C0 control character in an ``ORGANIZER`` or ``SENT-BY`` value parsed from jCal, which made :meth:`Component.to_xcal() <icalendar.cal.component.Component.to_xcal>` produce XML that is not well-formed. The value now goes through the same filter as the text and URI types, and that filter also removes U+FFFE and U+FFFF. I used AI (Claude Opus 5.5, via Claude Code) to find this bug, write the fix, and write its tests; I reviewed and validated the output. @alhudz | |||
There was a problem hiding this comment.
Be concise and avoid run-on sentences from AI. Also use inline literals for characters.
| Fixed :meth:`vCalAddress.to_xcal() <icalendar.prop.cal_address.vCalAddress.to_xcal>` writing characters that are not allowed in XML, such as a C0 control character in an ``ORGANIZER`` or ``SENT-BY`` value parsed from jCal, which made :meth:`Component.to_xcal() <icalendar.cal.component.Component.to_xcal>` produce XML that is not well-formed. The value now goes through the same filter as the text and URI types, and that filter also removes U+FFFE and U+FFFF. I used AI (Claude Opus 5.5, via Claude Code) to find this bug, write the fix, and write its tests; I reviewed and validated the output. @alhudz | |
| Fixed :meth:`vCalAddress.to_xcal() <icalendar.prop.cal_address.vCalAddress.to_xcal>` from writing characters that are disallowed in XML. The value now goes through the same filter as the text and URI types, and that filter also removes ``U+FFFE`` and ``U+FFFF``. I used AI (Claude Opus 5.5, via Claude Code) to find this bug, write the fix, and write its tests; I reviewed and validated the output. @alhudz |
| """Match the characters that XML 1.0 does not allow in a document. | ||
|
|
||
| The ``Char`` production in section 2.2 of XML 1.0 allows only | ||
| ``#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]``. | ||
| This pattern matches the characters outside of it that a Python :class:`str` | ||
| can contain, apart from surrogates: | ||
|
|
||
| - the C0 control characters ``#x0`` to ``#x1F``, except tab, line feed and | ||
| carriage return, and | ||
| - the noncharacters ``#xFFFE`` and ``#xFFFF``. | ||
|
|
||
| Lone surrogates ``#xD800`` to ``#xDFFF`` are also outside ``Char``, but are not | ||
| matched here. They cannot be encoded as UTF-8, and ``to_ical()`` rejects them. | ||
|
|
||
| .. seealso:: | ||
|
|
||
| `XML 1.0, section 2.2 Characters <https://www.w3.org/TR/REC-xml/#charsets>`_ | ||
| """ |
There was a problem hiding this comment.
Reword to improve readability and merge seealso. Also Char is not an inline literal.
| """Match the characters that XML 1.0 does not allow in a document. | |
| The ``Char`` production in section 2.2 of XML 1.0 allows only | |
| ``#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]``. | |
| This pattern matches the characters outside of it that a Python :class:`str` | |
| can contain, apart from surrogates: | |
| - the C0 control characters ``#x0`` to ``#x1F``, except tab, line feed and | |
| carriage return, and | |
| - the noncharacters ``#xFFFE`` and ``#xFFFF``. | |
| Lone surrogates ``#xD800`` to ``#xDFFF`` are also outside ``Char``, but are not | |
| matched here. They cannot be encoded as UTF-8, and ``to_ical()`` rejects them. | |
| .. seealso:: | |
| `XML 1.0, section 2.2 Characters <https://www.w3.org/TR/REC-xml/#charsets>`_ | |
| """ | |
| """Match the characters that XML 1.0 disallows in a document. | |
| `XML 1.0, section 2.2, Characters <https://www.w3.org/TR/REC-xml/#charsets>`_, | |
| or Char, allows only ``#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | | |
| [#x10000-#x10FFFF]``. This pattern matches the characters outside of it that a | |
| Python string can contain, apart from surrogates: | |
| - the ``C0`` control characters ``#x0`` to ``#x1F``, except tab, line feed and | |
| carriage return, and | |
| - the noncharacters ``#xFFFE`` and ``#xFFFF``. | |
| Lone surrogates ``#xD800`` to ``#xDFFF`` are also outside Char, but are not | |
| matched here. They can't be encoded as UTF-8, and ``to_ical()`` rejects them. |
There was a problem hiding this comment.
Applied in 677cbc4. The suggestion range included the closing """, so I kept that line as it was.
Linked issue
Description
In #1888, vCalAddress is listed as the one class that can create invalid xCal output.
vCalAddress.to_xcal()writes its value into<cal-address>without the_to_valid_xml_stringfilter that vText, vUri and vUnknown use. Parsing an .ics file strips control characters, but jCal keeps them. Because of that, anORGANIZER/ATTENDEEvalue, or aSENT-BY/DELEGATED-FROM/MEMBERparameter, that contains something like U+0001 makesto_xcal()output bytes that no XML parser accepts, including our ownfrom_xcal:The filter itself also missed U+FFFE and U+FFFF. Both are outside the XML
Charproduction cited intest_text_escape.py, and they reach vText from ordinary UTF-8 .ics input, for exampleSUMMARY:a\xef\xbf\xbeb.The existing
test_invalid_xml_characters_are_replacedtest now also runs for vCalAddress and checks the two noncharacters. Lone surrogates are left alone: they already fail into_ical()and aren't specific to xCal.Checklist
Additional information
The updated test fails 82 cases without the fix and passes with it. The full suite passes (21393 passed), and
ruff checkandruff format --checkare clean on the touched files.