Skip to content

remove invalid XML characters from vCalAddress xCal output - #1969

Open
alhudz wants to merge 5 commits into
PyCalendar:mainfrom
alhudz:xcal-cal-address-invalid-chars
Open

alhudz wants to merge 5 commits into
PyCalendar:mainfrom
alhudz:xcal-cal-address-invalid-chars

Conversation

@alhudz

@alhudz alhudz commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

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_string filter that vText, vUri and vUnknown use. Parsing an .ics file strips control characters, but jCal keeps them. Because of that, an ORGANIZER/ATTENDEE value, or a SENT-BY/DELEGATED-FROM/MEMBER parameter, that contains something like U+0001 makes to_xcal() output bytes that no XML parser accepts, including our own from_xcal:

from xml.etree.ElementTree import fromstring
from icalendar import Calendar

cal = Calendar.from_jcal(["vcalendar", [], [["vevent", [
    ["organizer", {"sent-by": "mailto:s\u0002@x"}, "cal-address", "mailto:a\u0001b@example.com"],
], []]]])
fromstring(cal.to_xcal())  # ParseError: not well-formed (invalid token)

The filter itself also missed U+FFFE and U+FFFF. Both are outside the XML Char production cited in test_text_escape.py, and they reach vText from ordinary UTF-8 .ics input, for example SUMMARY:a\xef\xbf\xbeb.

The existing test_invalid_xml_characters_are_replaced test now also runs for vCalAddress and checks the two noncharacters. Lone surrogates are left alone: they already fail in to_ical() and aren't specific to xCal.

Checklist

  • I added a change log entry, following the instructions in all subsections under Change log requirements.
  • I followed icalendar's Artificial intelligence policy and disclosed my Responsible AI use in my commit messages, if applicable.
  • I added or updated tests, if applicable.
  • I ran and ensured all tests pass locally by following Run tests.
  • I added or edited documentation as necessary, both as docstrings to be rendered in the API documentation and narrative documentation, following the Style guide.

Additional information

The updated test fails 82 cases without the fix and passes with it. The full suite passes (21393 passed), and ruff check and ruff format --check are clean on the touched files.

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.
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the pull request, @alhudz! A maintainer will review it shortly.

@github-actions

Copy link
Copy Markdown
Contributor

This pull request did not pass quality checks and AI use is suspected. Please review Contribute and make any necessary amendments.

@github-actions github-actions Bot added the ai-suspicion This contribution is possibly created with lots of AI help without enough human understanding. label Oct 18, 2026
@alhudz

alhudz commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

@faresrafat3 It is re.sub: _to_valid_xml_string returns _VALID_XML_CHARACTERS.sub("", string) (src/icalendar/parser/xcal/string.py, unchanged by this PR), so U+FFFE and U+FFFF are removed, not rejected. test_invalid_xml_characters_are_replaced checks exactly that: the output equals the input with the invalid character dropped.

@niccokunzmann niccokunzmann left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. I have some questions.

Comment thread src/icalendar/parser/xcal/string.py Outdated


_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]")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, this needs a docstring saying why which characters are allowed.
With references to the source.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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&#55296;</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
niccokunzmann previously approved these changes Oct 22, 2026

@niccokunzmann niccokunzmann left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically fine. Let's have another docs review.

@alhudz

alhudz commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

#1966 added news/1888.bugfix on main, which conflicted with the entry here, so I've moved this one to news/1888.bugfix.1 and merged main in. That also brings in the Timezone.Timezone forward-reference fix the Read the Docs build was failing on, so there should be a preview for the docs review now. The code diff is unchanged, and the full suite and the docs build with -W pass locally.

@niccokunzmann the push dismissed your approval.

@stevepiercy stevepiercy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution, @alhuda! I've made a few suggestions. Would you please take care? Thank you!

Comment thread news/1888.bugfix.1 Outdated
@@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be concise and avoid run-on sentences from AI. Also use inline literals for characters.

Suggested change
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in 677cbc4.

Comment thread src/icalendar/parser/xcal/string.py Outdated
Comment on lines +37 to +54
"""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>`_
"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reword to improve readability and merge seealso. Also Char is not an inline literal.

Suggested change
"""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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in 677cbc4. The suggestion range included the closing """, so I kept that line as it was.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-suspicion This contribution is possibly created with lots of AI help without enough human understanding.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants