Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Security
--------

* Bound the length of author values passed to the author-email regex.
The pattern backtracks quadratically, so a hostile feed with a very long
``<author>`` or ``dc:creator`` value could make parsing consume seconds of
CPU. Long values now skip the email match instead.
12 changes: 11 additions & 1 deletion feedparser/mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@
r"(\?subject=\S+)?"
)

# Feeds are untrusted input, and the domain part of ``email_pattern``
# backtracks quadratically on long, almost-matching strings. Only run the
# search on values short enough to plausibly hold an address: RFC 5321 caps
# an address at 254 octets, and this leaves plenty of room for a display name
# alongside it. Anything longer is not a real author string, so skip it.
EMAIL_PATTERN_MAX_LENGTH = 1000


class XMLParserMixin(
_base.Namespace,
Expand Down Expand Up @@ -796,7 +803,10 @@ def _sync_author_detail(self, key="author"):
author, email = context.get(key), None
if not author:
return
emailmatch = email_pattern.search(author)
if len(author) <= EMAIL_PATTERN_MAX_LENGTH:
emailmatch = email_pattern.search(author)
else:
emailmatch = None
if emailmatch:
email = emailmatch.group(0)
# probably a better way to do the following, but it passes
Expand Down
41 changes: 41 additions & 0 deletions tests/test_author_email_bound.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from __future__ import annotations

import feedparser
from feedparser.mixin import EMAIL_PATTERN_MAX_LENGTH


def _feed(author: str) -> str:
return (
'<?xml version="1.0"?>'
'<rss version="2.0"><channel><title>t</title>'
f"<item><title>x</title><author>{author}</author></item>"
"</channel></rss>"
)


def test_normal_author_email_still_extracted():
result = feedparser.parse(_feed("Example editor (me@example.com)"))
detail = result.entries[0].author_detail
assert detail["name"] == "Example editor"
assert detail["email"] == "me@example.com"


def test_bare_email_still_extracted():
result = feedparser.parse(_feed("me@example.com"))
assert result.entries[0].author_detail["email"] == "me@example.com"


def test_oversized_author_skips_email_match():
# A hostile feed can supply an author value crafted so the email pattern
# backtracks quadratically. Values longer than the bound are left alone
# instead of being fed to the regex, so parsing stays fast.
hostile = "a@" + "a." * 16000 + "!"
assert len(hostile) > EMAIL_PATTERN_MAX_LENGTH

result = feedparser.parse(_feed(hostile))

assert result.bozo is False
# The oversized value is kept verbatim as the name and no email is split
# out of it.
assert "email" not in result.entries[0].author_detail
assert result.entries[0].author_detail["name"] == hostile