diff --git a/news/1362.bugfix b/news/1362.bugfix new file mode 100644 index 000000000..48b168eed --- /dev/null +++ b/news/1362.bugfix @@ -0,0 +1 @@ +Avoid implicit file reads from string calendar input in :meth:`~icalendar.cal.component.Component.from_ical` by removing support for passing file paths as strings. Use :class:`pathlib.Path` instead to read from a file. @uwezkhan diff --git a/src/icalendar/cal/component.py b/src/icalendar/cal/component.py index 51f63d9f3..05cf3d13a 100644 --- a/src/icalendar/cal/component.py +++ b/src/icalendar/cal/component.py @@ -490,12 +490,14 @@ def property_items( @overload @classmethod def from_ical( - cls, st: str | bytes, multiple: Literal[False] = False + cls, st: str | bytes | Path, multiple: Literal[False] = False ) -> Component: ... @overload @classmethod - def from_ical(cls, st: str | bytes, multiple: Literal[True]) -> list[Component]: ... + def from_ical( + cls, st: str | bytes | Path, multiple: Literal[True] + ) -> list[Component]: ... @classmethod def _get_ical_parser(cls, st: str | bytes) -> ComponentIcalParser: @@ -512,7 +514,7 @@ def from_ical( Parameters: st: iCalendar data as bytes or string, or a path to an iCalendar file as - :class:`pathlib.Path` or string. + :class:`pathlib.Path`. multiple: If ``True``, returns list. If ``False``, returns single component. Returns: @@ -523,14 +525,6 @@ def from_ical( """ if isinstance(st, Path): st = st.read_bytes() - elif isinstance(st, str) and "\n" not in st and "\r" not in st: - path = Path(st) - try: - is_file = path.is_file() - except OSError: - is_file = False - if is_file: - st = path.read_bytes() parser = cls._get_ical_parser(st) components = parser.parse() if multiple: diff --git a/src/icalendar/tests/test_issue_756_read_calendar_from_file.py b/src/icalendar/tests/test_issue_756_read_calendar_from_file.py index 83daec588..cc2d31ccb 100644 --- a/src/icalendar/tests/test_issue_756_read_calendar_from_file.py +++ b/src/icalendar/tests/test_issue_756_read_calendar_from_file.py @@ -28,12 +28,25 @@ def test_reading_cal_from_path(dummy_cal): assert actual_cal.to_ical() == expected_cal.to_ical() -def test_reading_cal_from_string_path(dummy_cal): - """Test reading a calendar from a valid string path.""" - expected_cal, path = dummy_cal - actual_cal = Calendar.from_ical(str(path)) +def test_string_path_is_parsed_as_calendar_data(dummy_cal): + """Test that string input is not interpreted as a filesystem path.""" + _, path = dummy_cal - assert actual_cal.to_ical() == expected_cal.to_ical() + with pytest.raises(ValueError) as exc_info: + Calendar.from_ical(str(path)) + + assert "BEGIN:VCALENDAR" not in str(exc_info.value) + + +def test_string_path_does_not_disclose_file_contents(tmp_path: Path): + """Test that parsing a string path does not read from the filesystem.""" + path = tmp_path / "secret.txt" + path.write_text("SECRET-CONTENT-WITHOUT-NEWLINE", encoding="utf-8") + + with pytest.raises(ValueError) as exc_info: + Calendar.from_ical(str(path)) + + assert "SECRET-CONTENT-WITHOUT-NEWLINE" not in str(exc_info.value) def test_reading_cal_from_long_string(dummy_cal):