CVE-2021-29471 (Python, published 2021-05-11T15:15Z)
Description: value: Synapse is a Matrix reference homeserver written in python (pypi package matrix-synapse). Matrix is an ecosystem for open federated Instant Messaging and VoIP. In Synapse before version 1.33.2 "Push rules" can specify conditions under which they will match, including `event_match`, which matches event content against a pattern including wildcards. Certain patterns can cause very poor performance in the matching engine, leading to a denial-of-service when processing moderate length events. The issue is patched in version 1.33.2. A potential workaround might be to prevent users from making custom push rules, by blocking such requests at a reverse-proxy.
Severity: MEDIUM (CVSS v3 base score: 5.3)
Weakness: CWE-331 \u2014 Insufficient Entropy
The product uses an algorithm or scheme that produces insufficient entropy, leaving patterns or clusters of values that are more likely to occur than others.
Repository: https://github.com/matrix-org/synapse
Files changed:
synapse/config/tls.py
synapse/push/push_rule_evaluator.py
synapse/util/__init__.py
tests/federation/test_federation_server.py
tests/push/test_push_rule_evaluator.py
tests/util/test_glob_to_regex.py
Commit message: Merge pull request from GHSA-x345-32rc-8h85

* tests for push rule pattern matching

* tests for acl pattern matching

* factor out common `re.escape`

* Factor out common re.compile

* Factor out common anchoring code

* add word_boundary support to `glob_to_regex`

* Use `glob_to_regex` in push rule evaluator

NB that this drops support for character classes. I don't think anyone ever
used them.

* Improve efficiency of globs with multiple wildcards

The idea here is that we compress multiple `*` globs into a single `.*`. We
also need to consider `?`, since `*?*` is as hard to implement efficiently as
`**`.

* add assertion on regex pattern

* Fix mypy

* Simplify glob_to_regex

* Inline the glob_to_regex helper function

Signed-off-by: Dan Callahan <danc@element.io>

* Moar comments

Signed-off-by: Dan Callahan <danc@element.io>

Co-authored-by: Dan Callahan <danc@element.io>

Vulnerable code:
from typing import List, Optional
        self.federation_certificate_verification_whitelist = []  # type: List[str]
            r1 = _re_word_boundary(r1)
            r = _glob_to_re(glob, word_boundary)
def _glob_to_re(glob: str, word_boundary: bool) -> Pattern:
    """Generates regex for a given glob.

    Args:
        glob
        word_boundary: Whether to match against word boundaries or entire string.
    """
    if IS_GLOB.search(glob):
        r = re.escape(glob)

        r = r.replace(r"\*", ".*?")
        r = r.replace(r"\?", ".")

        # handle [abc], [a-z] and [!a-z] style ranges.
        r = GLOB_REGEX.sub(
            lambda x: (
                "[%s%s]" % (x.group(1) and "^" or "", x.group(2).replace(r"\\\-", "-"))
            ),
            r,
        )
        if word_boundary:
            r = _re_word_boundary(r)

            return re.compile(r, flags=re.IGNORECASE)
        else:
            r = "^" + r + "$"

            return re.compile(r, flags=re.IGNORECASE)
    elif word_boundary:
        r = re.escape(glob)
        r = _re_word_boundary(r)

        return re.compile(r, flags=re.IGNORECASE)
    else:
        r = "^" + re.escape(glob) + "$"
        return re.compile(r, flags=re.IGNORECASE)


def _re_word_boundary(r: str) -> str:
    """
    Adds word boundary characters to the start and end of an
    expression to require that the match occur as a whole word,
    but do so respecting the fact that strings starting or ending
    with non-word characters will change word boundaries.
    """
    # we can't use \b as it chokes on unicode. however \W seems to be okay
    # as shorthand for [^0-9A-Za-z_].
    return r"(^|\W)%s(\W|$)" % (r,)


def glob_to_regex(glob):
    The regex is anchored at the beginning and end of the string.

        glob (str)
        re.RegexObject
    res = ""
    for c in glob:
        if c == "*":
            res = res + ".*"
        elif c == "?":
            res = res + "."
            res = res + re.escape(c)
    # \A anchors at start of string, \Z at end of string
    return re.compile(r"\A" + res + r"\Z", re.IGNORECASE)

Fixed code:
from typing import List, Optional, Pattern
        self.federation_certificate_verification_whitelist = []  # type: List[Pattern]
from synapse.util import glob_to_regex, re_word_boundary
            r1 = re_word_boundary(r1)
            r = glob_to_regex(glob, word_boundary)
from typing import Pattern
_WILDCARD_RUN = re.compile(r"([\?\*]+)")


def glob_to_regex(glob: str, word_boundary: bool = False) -> Pattern:
        glob: pattern to match
        word_boundary: If True, the pattern will be allowed to match at word boundaries
           anywhere in the string. Otherwise, the pattern is anchored at the start and
           end of the string.
        compiled regex pattern

    # Patterns with wildcards must be simplified to avoid performance cliffs
    # - The glob `?**?**?` is equivalent to the glob `???*`
    # - The glob `???*` is equivalent to the regex `.{3,}`
    chunks = []
    for chunk in _WILDCARD_RUN.split(glob):
        # No wildcards? re.escape()
        if not _WILDCARD_RUN.match(chunk):
            chunks.append(re.escape(chunk))
            continue

        # Wildcards? Simplify.
        qmarks = chunk.count("?")
        if "*" in chunk:
            chunks.append(".{%d,}" % qmarks)
            chunks.append(".{%d}" % qmarks)

    res = "".join(chunks)
    if word_boundary:
        res = re_word_boundary(res)
    else:
        # \A anchors at start of string, \Z at end of string
        res = r"\A" + res + r"\Z"

    return re.compile(res, re.IGNORECASE)


def re_word_boundary(r: str) -> str:
    """
    Adds word boundary characters to the start and end of an
    expression to require that the match occur as a whole word,
    but do so respecting the fact that strings starting or ending
    with non-word characters will change word boundaries.
    """
    # we can't use \b as it chokes on unicode. however \W seems to be okay
    # as shorthand for [^0-9A-Za-z_].
    return r"(^|\W)%s(\W|$)" % (r,)
    def test_wildcard_matching(self):
        e = _create_acl_event({"allow": ["good*.com"]})
        self.assertTrue(
            server_matches_acl_event("good.com", e),
            "* matches 0 characters",
        )
        self.assertTrue(
            server_matches_acl_event("GOOD.COM", e),
            "pattern is case-insensitive",
        )
        self.assertTrue(
            server_matches_acl_event("good.aa.com", e),
            "* matches several characters, including '.'",
        )
        self.assertFalse(
            server_matches_acl_event("ishgood.com", e),
            "pattern does not allow prefixes",
        )

from typing import Any, Dict

    def _assert_matches(
        self, condition: Dict[str, Any], content: Dict[str, Any], msg=None
    ) -> None:
        evaluator = self._get_evaluator(content)
        self.assertTrue(evaluator.matches(condition, "@user:test", "display_name"), msg)

    def _assert_not_matches(
        self, condition: Dict[str, Any], content: Dict[str, Any], msg=None
    ) -> None:
        evaluator = self._get_evaluator(content)
        self.assertFalse(
            evaluator.matches(condition, "@user:test", "display_name"), msg
        )

    def test_event_match_body(self):
        """Check that event_match conditions on content.body work as expected"""

        # if the key is `content.body`, the pattern matches substrings.

        # non-wildcards should match
        condition = {
            "kind": "event_match",
            "key": "content.body",
            "pattern": "foobaz",
        }
        self._assert_matches(
            condition,
            {"body": "aaa FoobaZ zzz"},
            "patterns should match and be case-insensitive",
        )
        self._assert_not_matches(
            condition,
            {"body": "aa xFoobaZ yy"},
            "pattern should only match at word boundaries",
        )
        self._assert_not_matches(
            condition,
            {"body": "aa foobazx yy"},
            "pattern should only match at word boundaries",
        )

        # wildcards should match
        condition = {
            "kind": "event_match",
            "key": "content.body",
            "pattern": "f?o*baz",
        }

        self._assert_matches(
            condition,
            {"body": "aaa FoobarbaZ zzz"},
            "* should match string and pattern should be case-insensitive",
        )
        self._assert_matches(
            condition, {"body": "aa foobaz yy"}, "* should match 0 characters"
        )
        self._assert_not_matches(
            condition, {"body": "aa fobbaz yy"}, "? should not match 0 characters"
        )
        self._assert_not_matches(
            condition, {"body": "aa fiiobaz yy"}, "? should not match 2 characters"
        )
        self._assert_not_matches(
            condition,
            {"body": "aa xfooxbaz yy"},
            "pattern should only match at word boundaries",
        )
        self._assert_not_matches(
            condition,
            {"body": "aa fooxbazx yy"},
            "pattern should only match at word boundaries",
        )

        # test backslashes
        condition = {
            "kind": "event_match",
            "key": "content.body",
            "pattern": r"f\oobaz",
        }
        self._assert_matches(
            condition,
            {"body": r"F\oobaz"},
            "backslash should match itself",
        )
        condition = {
            "kind": "event_match",
            "key": "content.body",
            "pattern": r"f\?obaz",
        }
        self._assert_matches(
            condition,
            {"body": r"F\oobaz"},
            r"? after \ should match any character",
        )

    def test_event_match_non_body(self):
        """Check that event_match conditions on other keys work as expected"""

        # if the key is anything other than 'content.body', the pattern must match the
        # whole value.

        # non-wildcards should match
        condition = {
            "kind": "event_match",
            "key": "content.value",
            "pattern": "foobaz",
        }
        self._assert_matches(
            condition,
            {"value": "FoobaZ"},
            "patterns should match and be case-insensitive",
        )
        self._assert_not_matches(
            condition,
            {"value": "xFoobaZ"},
            "pattern should only match at the start/end of the value",
        )
        self._assert_not_matches(
            condition,
            {"value": "FoobaZz"},
            "pattern should only match at the start/end of the value",
        )

        # wildcards should match
        condition = {
            "kind": "event_match",
            "key": "content.value",
            "pattern": "f?o*baz",
        }
        self._assert_matches(
            condition,
            {"value": "FoobarbaZ"},
            "* should match string and pattern should be case-insensitive",
        )
        self._assert_matches(
            condition, {"value": "foobaz"}, "* should match 0 characters"
        )
        self._assert_not_matches(
            condition, {"value": "fobbaz"}, "? should not match 0 characters"
        )
        self._assert_not_matches(
            condition, {"value": "fiiobaz"}, "? should not match 2 characters"
        )
        self._assert_not_matches(
            condition,
            {"value": "xfooxbaz"},
            "pattern should only match at the start/end of the value",
        )
        self._assert_not_matches(
            condition,
            {"value": "fooxbazx"},
            "pattern should only match at the start/end of the value",
        )
        self._assert_not_matches(
            condition,
            {"value": "x\nfooxbaz"},
            "pattern should not match after a newline",
        )
        self._assert_not_matches(
            condition,
            {"value": "fooxbaz\nx"},
            "pattern should not match before a newline",
        )

# Copyright 2021 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from synapse.util import glob_to_regex

from tests.unittest import TestCase


class GlobToRegexTestCase(TestCase):
    def test_literal_match(self):
        """patterns without wildcards should match"""
        pat = glob_to_regex("foobaz")
        self.assertTrue(
            pat.match("FoobaZ"), "patterns should match and be case-insensitive"
        )
        self.assertFalse(
            pat.match("x foobaz"), "pattern should not match at word boundaries"
        )

    def test_wildcard_match(self):
        pat = glob_to_regex("f?o*baz")

        self.assertTrue(
            pat.match("FoobarbaZ"),
            "* should match string and pattern should be case-insensitive",
        )
        self.assertTrue(pat.match("foobaz"), "* should match 0 characters")
        self.assertFalse(pat.match("fooxaz"), "the character after * must match")
        self.assertFalse(pat.match("fobbaz"), "? should not match 0 characters")
        self.assertFalse(pat.match("fiiobaz"), "? should not match 2 characters")

    def test_multi_wildcard(self):
        """patterns with multiple wildcards in a row should match"""
        pat = glob_to_regex("**baz")
        self.assertTrue(pat.match("agsgsbaz"), "** should match any string")
        self.assertTrue(pat.match("baz"), "** should match the empty string")
        self.assertEqual(pat.pattern, r"\A.{0,}baz\Z")

        pat = glob_to_regex("*?baz")
        self.assertTrue(pat.match("agsgsbaz"), "*? should match any string")
        self.assertTrue(pat.match("abaz"), "*? should match a single char")
        self.assertFalse(pat.match("baz"), "*? should not match the empty string")
        self.assertEqual(pat.pattern, r"\A.{1,}baz\Z")

        pat = glob_to_regex("a?*?*?baz")
        self.assertTrue(pat.match("a g baz"), "?*?*? should match 3 chars")
        self.assertFalse(pat.match("a..baz"), "?*?*? should not match 2 chars")
        self.assertTrue(pat.match("a.gg.baz"), "?*?*? should match 4 chars")
        self.assertEqual(pat.pattern, r"\Aa.{3,}baz\Z")