Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-Protego for openSUSE:Factory checked in at 2026-07-07 21:02:09 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-Protego (Old) and /work/SRC/openSUSE:Factory/.python-Protego.new.1982 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-Protego" Tue Jul 7 21:02:09 2026 rev:10 rq:1364107 version:0.6.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-Protego/python-Protego.changes 2026-02-18 17:12:11.978634218 +0100 +++ /work/SRC/openSUSE:Factory/.python-Protego.new.1982/python-Protego.changes 2026-07-07 21:04:03.796050809 +0200 @@ -1,0 +2,17 @@ +Mon Jul 6 19:29:28 UTC 2026 - Dirk Müller <[email protected]> + +- update to 0.6.2 (CVE-2026-55520, bsc#1270394): + * Fixed a ReDoS (regular expression denial of service) + vulnerability: URL patterns from robots.txt Allow and + Disallow directives were compiled into regular expressions, + where multiple * wildcards could cause exponential + backtracking. A server could exploit this to cause denial of + service by serving a crafted robots.txt file. Wildcard + matching is now performed without regular expressions. + Please, see the CVE-2026-55520 and GHSA-wjmf-p669-5m5p + security advisories for more information. + * Fixed parsing of Request-rate values where the seconds field + has no time-unit suffix (e.g. 1/60 instead of 1/60s). + Previously the last digit of the number was silently dropped. + +------------------------------------------------------------------- Old: ---- protego-0.6.0.tar.gz New: ---- protego-0.6.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-Protego.spec ++++++ --- /var/tmp/diff_new_pack.O3vi8J/_old 2026-07-07 21:04:04.504075323 +0200 +++ /var/tmp/diff_new_pack.O3vi8J/_new 2026-07-07 21:04:04.508075462 +0200 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-Protego -Version: 0.6.0 +Version: 0.6.2 Release: 0 Summary: Pure-Python robotstxt parser with support for modern conventions License: BSD-3-Clause ++++++ protego-0.6.0.tar.gz -> protego-0.6.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/protego-0.6.0/CHANGELOG.rst new/protego-0.6.2/CHANGELOG.rst --- old/protego-0.6.0/CHANGELOG.rst 2020-02-02 01:00:00.000000000 +0100 +++ new/protego-0.6.2/CHANGELOG.rst 2020-02-02 01:00:00.000000000 +0100 @@ -2,6 +2,28 @@ Protego changelog ================= +0.6.2 (2026-06-25) +================== + +- Fixed a ReDoS (regular expression denial of service) vulnerability: URL + patterns from ``robots.txt`` ``Allow`` and ``Disallow`` directives were + compiled into regular expressions, where multiple ``*`` wildcards could + cause exponential backtracking. A server could exploit this to cause denial + of service by serving a crafted ``robots.txt`` file. Wildcard matching is + now performed without regular expressions. Please, see the + `CVE-2026-55520`_ and `GHSA-wjmf-p669-5m5p`_ security advisories for more + information. + +.. _CVE-2026-55520: https://www.cve.org/CVERecord?id=CVE-2026-55520 +.. _GHSA-wjmf-p669-5m5p: https://github.com/scrapy/protego/security/advisories/GHSA-wjmf-p669-5m5p + +0.6.1 (2026-06-11) +================== + +- Fixed parsing of ``Request-rate`` values where the seconds field has no + time-unit suffix (e.g. ``1/60`` instead of ``1/60s``). Previously the last + digit of the number was silently dropped. + 0.6.0 (2026-01-29) ================== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/protego-0.6.0/PKG-INFO new/protego-0.6.2/PKG-INFO --- old/protego-0.6.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 +++ new/protego-0.6.2/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: Protego -Version: 0.6.0 +Version: 0.6.2 Summary: Pure-Python robots.txt parser with support for modern conventions Project-URL: Homepage, https://github.com/scrapy/protego Project-URL: Source, https://github.com/scrapy/protego diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/protego-0.6.0/pyproject.toml new/protego-0.6.2/pyproject.toml --- old/protego-0.6.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 +++ new/protego-0.6.2/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 @@ -4,7 +4,7 @@ [project] name = "Protego" -version = "0.6.0" +version = "0.6.2" description = "Pure-Python robots.txt parser with support for modern conventions" authors = [ { name = "Anubhav Patel", email = "[email protected]" }, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/protego-0.6.0/src/protego/_ruleset.py new/protego-0.6.2/src/protego/_ruleset.py --- old/protego-0.6.0/src/protego/_ruleset.py 2020-02-02 01:00:00.000000000 +0100 +++ new/protego-0.6.2/src/protego/_ruleset.py 2020-02-02 01:00:00.000000000 +0100 @@ -123,7 +123,12 @@ requests_str, seconds_str = rate.split("/") time_unit = seconds_str[-1].lower() - requests, seconds = int(requests_str), int(seconds_str[:-1]) + if time_unit in ("s", "m", "h", "d"): + seconds = int(seconds_str[:-1]) + else: + time_unit = "s" + seconds = int(seconds_str) + requests = int(requests_str) if time_unit == "m": seconds *= 60 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/protego-0.6.0/src/protego/_urlpattern.py new/protego-0.6.2/src/protego/_urlpattern.py --- old/protego-0.6.0/src/protego/_urlpattern.py 2020-02-02 01:00:00.000000000 +0100 +++ new/protego-0.6.2/src/protego/_urlpattern.py 2020-02-02 01:00:00.000000000 +0100 @@ -2,8 +2,6 @@ import re -_WILDCARDS = {"*", "$"} - class _URLPattern: """Internal class which represents a URL pattern.""" @@ -18,40 +16,48 @@ self._pattern_before_asterisk: str = self._pattern[ : self._pattern.find("*") ] + self._wildcard_parts, self._wildcard_ends_with_dollar = ( + self._prepare_wildcard_parts(pattern) + ) elif self._contains_dollar: self._pattern_before_dollar: str = self._pattern[:-1] - self._pattern_compiled: re.Pattern[str] | None = None - def match(self, url: str) -> bool: """Return True if pattern matches the given URL, otherwise return False.""" - # check if pattern is already compiled - if self._pattern_compiled is not None: - return bool(self._pattern_compiled.match(url)) - if not self._contains_asterisk: if not self._contains_dollar: - # answer directly for patterns without wildcards return url.startswith(self._pattern) - - # pattern only contains $ wildcard. return url == self._pattern_before_dollar if not url.startswith(self._pattern_before_asterisk): return False - _pattern_regex = self._prepare_pattern_for_regex(self._pattern) - self._pattern_compiled = re.compile(_pattern_regex) - return bool(self._pattern_compiled.match(url)) + return self._match_wildcard(url) + + def _match_wildcard(self, url: str) -> bool: + parts = self._wildcard_parts + ends_with_dollar = self._wildcard_ends_with_dollar + + pos = len(parts[0]) + + for part in parts[1:-1]: + idx = url.find(part, pos) + if idx == -1: + return False + pos = idx + len(part) + + last = parts[-1] + if ends_with_dollar: + end_pos = len(url) - len(last) + return end_pos >= pos and url[end_pos:] == last + if not last: + return True + return url.find(last, pos) != -1 @staticmethod - def _prepare_pattern_for_regex(pattern: str) -> str: - """Return equivalent regex pattern for the given URL pattern.""" + def _prepare_wildcard_parts(pattern: str) -> tuple[list[str], bool]: + ends_with_dollar = pattern.endswith("$") + if ends_with_dollar: + pattern = pattern[:-1] pattern = re.sub(r"\*+", "*", pattern) - s = re.split(r"(\*|\$$)", pattern) - for index, substr in enumerate(s): - if substr not in _WILDCARDS: - s[index] = re.escape(substr) - elif s[index] == "*": - s[index] = ".*?" - return "".join(s) + return pattern.split("*"), ends_with_dollar diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/protego-0.6.0/tests/test_protego.py new/protego-0.6.2/tests/test_protego.py --- old/protego-0.6.0/tests/test_protego.py 2020-02-02 01:00:00.000000000 +0100 +++ new/protego-0.6.2/tests/test_protego.py 2020-02-02 01:00:00.000000000 +0100 @@ -261,6 +261,52 @@ assert req_rate.end_time.hour == 19 assert req_rate.end_time.minute == 40 + def test_request_rate_bare_number(self): + content = """ + User-agent: one + Request-rate: 1/60 + User-agent: two + Request-rate: 10/60 + User-agent: three + Request-rate: 90/180 + User-agent: four + Request-rate: 1/5 + User-agent: five + Request-rate: 1/10 0600-0845 + """ + rp = Protego.parse(content=content) + + req_rate = rp.request_rate("one") + assert req_rate is not None + assert req_rate.requests == 1 + assert req_rate.seconds == 60 + + req_rate = rp.request_rate("two") + assert req_rate is not None + assert req_rate.requests == 10 + assert req_rate.seconds == 60 + + req_rate = rp.request_rate("three") + assert req_rate is not None + assert req_rate.requests == 90 + assert req_rate.seconds == 180 + + req_rate = rp.request_rate("four") + assert req_rate is not None + assert req_rate.requests == 1 + assert req_rate.seconds == 5 + + req_rate = rp.request_rate("five") + assert req_rate is not None + assert req_rate.requests == 1 + assert req_rate.seconds == 10 + assert req_rate.start_time is not None + assert req_rate.start_time.hour == 6 + assert req_rate.start_time.minute == 0 + assert req_rate.end_time is not None + assert req_rate.end_time.hour == 8 + assert req_rate.end_time.minute == 45 + def test_no_request_rate(self): content = """ User-agent: one @@ -1063,3 +1109,15 @@ content = f"User-Agent: *\nallow: {allow}\ndisallow: {disallow}\n" rp = Protego.parse(content) assert rp.can_fetch(url, "*") == allowed + + [email protected](2) +def test_redos(): + # A pattern with many wildcards against a near-miss URL causes catastrophic + # backtracking with a naive regex approach. 32 wildcards are enough to hang + # for minutes; the fix resolves it in microseconds. + disallow = "/a/" + "*a" * 32 + "b" + url = "http://example.com/a/" + "a" * 32 + "c" + content = f"User-agent: *\nDisallow: {disallow}\n" + rp = Protego.parse(content) + assert rp.can_fetch(url, "*") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/protego-0.6.0/tox.ini new/protego-0.6.2/tox.ini --- old/protego-0.6.0/tox.ini 2020-02-02 01:00:00.000000000 +0100 +++ new/protego-0.6.2/tox.ini 2020-02-02 01:00:00.000000000 +0100 @@ -5,6 +5,7 @@ deps = pytest pytest-cov >= 7.0.0 + pytest-timeout commands = pytest --cov=protego --cov-report=xml --cov-report= {posargs:tests}
