diff --git a/changelog.d/_599-parse-raises-on-malformed-input.rst b/changelog.d/_599-parse-raises-on-malformed-input.rst new file mode 100644 index 000000000..4cb61771e --- /dev/null +++ b/changelog.d/_599-parse-raises-on-malformed-input.rst @@ -0,0 +1,7 @@ +Fixed +----- + +* Stop ``parse()`` from raising on a backslash in a DOCTYPE entity value, + on character references outside the Unicode range, and on a GML + ``srsName`` with a non-numeric EPSG code. Backslashes in entity values + are also no longer treated as escapes. (#599) diff --git a/feedparser/mixin.py b/feedparser/mixin.py index 3acee1b10..ca3db012e 100644 --- a/feedparser/mixin.py +++ b/feedparser/mixin.py @@ -373,6 +373,9 @@ def handle_charref(self, ref): c = int(ref[1:], 16) else: c = int(ref) + if c > 0x10FFFF or 0xD800 <= c <= 0xDFFF: + # Not a valid Unicode scalar value; use U+FFFD like HTML parsers. + c = 0xFFFD text = chr(c).encode("utf-8") self.elementstack[-1][2].append(text) diff --git a/feedparser/namespaces/georss.py b/feedparser/namespaces/georss.py index c2d9eb09a..8d2fc699d 100644 --- a/feedparser/namespaces/georss.py +++ b/feedparser/namespaces/georss.py @@ -125,8 +125,7 @@ def _end_gml_pos(self): srs_dimension = context["where"].get("srsDimension", 2) swap = True if srs_name and "EPSG" in srs_name: - epsg = int(srs_name.split(":")[-1]) - swap = bool(epsg in _geogCS) + swap = _epsg_is_geographic(srs_name) geometry = _parse_georss_point(this, swap=swap, dims=srs_dimension) if geometry: self._save_where(geometry) @@ -141,8 +140,7 @@ def _end_gml_poslist(self): srs_dimension = context["where"].get("srsDimension", 2) swap = True if srs_name and "EPSG" in srs_name: - epsg = int(srs_name.split(":")[-1]) - swap = bool(epsg in _geogCS) + swap = _epsg_is_geographic(srs_name) geometry = _parse_poslist(this, self.ingeometry, swap=swap, dims=srs_dimension) if geometry: self._save_where(geometry) @@ -190,6 +188,18 @@ def _gen_georss_coords(value, swap=True, dims=2): return +def _epsg_is_geographic(srs_name): + """Return True if an EPSG srsName names a geographic CRS. + + Codes that are not integers are treated like a missing srsName. + """ + try: + epsg = int(srs_name.split(":")[-1]) + except ValueError: + return True + return epsg in _geogCS + + def _parse_georss_point(value, swap=True, dims=2): # A point contains a single latitude-longitude pair, separated by # whitespace. We'll also handle comma separators. diff --git a/feedparser/sanitizer.py b/feedparser/sanitizer.py index 0008e6d59..5c6893401 100644 --- a/feedparser/sanitizer.py +++ b/feedparser/sanitizer.py @@ -967,7 +967,9 @@ def replace_doctype(data: bytes) -> tuple[str | None, bytes, dict[str, str]]: + b">\n\n]>" ) - data = RE_DOCTYPE_PATTERN.sub(replacement, head) + data + # Use a function so backslashes in entity values are not treated as + # escapes or group references by re.sub(). + data = RE_DOCTYPE_PATTERN.sub(lambda _: replacement, head) + data # Precompute the safe entities for the loose parser. entities = { diff --git a/tests/illformed/charref_out_of_range.xml b/tests/illformed/charref_out_of_range.xml new file mode 100644 index 000000000..46fc72dbf --- /dev/null +++ b/tests/illformed/charref_out_of_range.xml @@ -0,0 +1,9 @@ + + + +a � b � c � d + + diff --git a/tests/wellformed/geo/gml_point_epsg_not_numeric.xml b/tests/wellformed/geo/gml_point_epsg_not_numeric.xml new file mode 100644 index 000000000..96bf4b0bf --- /dev/null +++ b/tests/wellformed/geo/gml_point_epsg_not_numeric.xml @@ -0,0 +1,16 @@ + + + + + + 36.9382 31.1732 + + + + diff --git a/tests/wellformed/rss/entity_in_doctype_backslash.xml b/tests/wellformed/rss/entity_in_doctype_backslash.xml new file mode 100644 index 000000000..846997975 --- /dev/null +++ b/tests/wellformed/rss/entity_in_doctype_backslash.xml @@ -0,0 +1,16 @@ + + + +]> + + + + +&path; + + +