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}
 

Reply via email to