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;
+
+
+