CVE-2024-28865: Insufficient Information (NVD-CWE-noinfo)
value: django-wiki is a wiki system for Django. Installations of django-wiki prior to version 0.10.1 are vulnerable to maliciously crafted article content that can cause severe use of server CPU through a regular expression loop. Version 0.10.1 fixes this issue. As a workaround, close off access to create and edit articles by anonymous users.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-18T22:15Z
Repository: https://github.com/django-wiki/django-wiki
Language: Python
Files changed: docs/release_notes.rst, src/wiki/core/markdown/mdx/codehilite.py, src/wiki/plugins/macros/mdx/macro.py, src/wiki/templatetags/wiki_tags.py, tests/plugins/macros/test_toc.py
Commit date: 2024-03-16 14:20:40 +0100
Commit message: Merge pull request from GHSA-wj85-w4f4-xh8h (#1350)

* ReDos: Apply `re.escape` to unsafe variable `keyword` before passing to `re.compile`

* Replace ReDos-vulnerable regex with the one from upstream Python-Markdown

* ReDos backtracking fix for macro plugin

* Add release note for GHSA-wj85-w4f4-xh8h

* Bump release date

* Bump version
Diff: diff --git a/docs/release_notes.rst b/docs/release_notes.rst
index c72a951f..9e1ecc27 100644
--- a/docs/release_notes.rst
+++ b/docs/release_notes.rst
@@ -66,10 +66,21 @@ Removed
 
 * Removes support for Django 2.2, 3.0, 3.1
 * Removes support for Python 3.7, 3.8, 3.9
 
 
+0.10.1
+------
+
+Released on 2024-03-16
+
+Security
+~~~~~~~~
+
+* Fixes reDOS issues: Denial of Service possible through unsafe regular expressions `GHSA-wj85-w4f4-xh8h <https://github.com/django-wiki/django-wiki/security/advisories/GHSA-wj85-w4f4-xh8h>`__ (Santos Gallegos, Benjamin Balder Bach)
+
+
 0.10
 ----
 
 Released on 2023-05-15
 
diff --git a/src/wiki/core/markdown/mdx/codehilite.py b/src/wiki/core/markdown/mdx/codehilite.py
index eab0fef4..856dc85e 100644
--- a/src/wiki/core/markdown/mdx/codehilite.py
+++ b/src/wiki/core/markdown/mdx/codehilite.py
@@ -1,14 +1,17 @@
+from textwrap import dedent
+
 import logging
 import re
 
 from markdown.extensions.codehilite import CodeHilite
 from markdown.extensions.codehilite import CodeHiliteExtension
 from markdown.preprocessors import Preprocessor
 from markdown.treeprocessors import Treeprocessor
 from wiki.core.markdown import add_to_registry
 
+
 logger = logging.getLogger(__name__)
 
 
 def highlight(code, config, tab_length, lang=None):
     code = CodeHilite(
@@ -33,18 +36,21 @@ class WikiFencedBlockPreprocessor(Preprocessor):
     directly and without configuration options invoke the vanilla CodeHilite
     extension.
     """
 
     FENCED_BLOCK_RE = re.compile(
-        r"""
-(?P<fence>^(?:~{3,}|`{3,}))[ ]*         # Opening ``` or ~~~
-(\{?\.?(?P<lang>[a-zA-Z0-9_+-]*))?[ ]*  # Optional {, and lang
-# Optional highlight lines, single- or double-quote-delimited
-(hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot))?[ ]*
-}?[ ]*\n                                # Optional closing }
-(?P<code>.*?)(?<=\n)
-(?P=fence)[ ]*$""",
+        dedent(
+            r"""
+            (?P<fence>^(?:~{3,}|`{3,}))[ ]*                          # opening fence
+            ((\{(?P<attrs>[^\}\n]*)\})|                              # (optional {attrs} or
+            (\.?(?P<lang>[\w#.+-]*)[ ]*)?                            # optional (.)lang
+            (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot)[ ]*)?) # optional hl_lines)
+            \n                                                       # newline (end of opening fence)
+            (?P<code>.*?)(?<=\n)                                     # the code block
+            (?P=fence)[ ]*$                                          # closing fence
+        """
+        ),
         re.MULTILINE | re.DOTALL | re.VERBOSE,
     )
     CODE_WRAP = "<pre>%s</pre>"
 
     def __init__(self, md):
diff --git a/src/wiki/plugins/macros/mdx/macro.py b/src/wiki/plugins/macros/mdx/macro.py
index bebbca43..dd147cf7 100644
--- a/src/wiki/plugins/macros/mdx/macro.py
+++ b/src/wiki/plugins/macros/mdx/macro.py
@@ -9,11 +9,15 @@ from wiki.plugins.macros.mdx import toc
 
 # See:
 # http://stackoverflow.com/questions/430759/regex-for-managing-escaped-characters-for-items-like-string-literals
 re_sq_short = r"'([^'\\]*(?:\\.[^'\\]*)*)'"
 
-MACRO_RE = r"(\[(?P<macro>\w+)(?P<kwargs>\s\w+\:.+)*\])"
+
+MACRO_RE = (
+    r"""\[(?P<macro>\w+)(?P<kwargs>(\s+\w+\:([^\:\]\s]+|'[^']+'))+)*\]"""
+)
+
 KWARG_RE = re.compile(
     r"\s*(?P<arg>\w+)(:(?P<value>([^\']+|%s)))?" % re_sq_short, re.IGNORECASE
 )
 
 
@@ -46,14 +50,16 @@ class MacroPattern(markdown.inlinepatterns.Pattern):
             return m.group(2)
 
         kwargs = m.group("kwargs")
         if not kwargs:
             return getattr(self, macro)()
+
         kwargs_dict = {}
         for kwarg in KWARG_RE.finditer(kwargs):
             arg = kwarg.group("arg")
             value = kwarg.group("value")
+
             if value is None:
                 value = True
             if isinstance(value, str):
                 # If value is enclosed with ': Remove and
                 # remove escape sequences
diff --git a/src/wiki/templatetags/wiki_tags.py b/src/wiki/templatetags/wiki_tags.py
index f5c46db3..7fbeda39 100644
--- a/src/wiki/templatetags/wiki_tags.py
+++ b/src/wiki/templatetags/wiki_tags.py
@@ -127,11 +127,11 @@ def get_content_snippet(content, keyword, max_words=30):
         before_words = all_before[-max_words // 2 :]
         after_words = all_after[: max_words - len(before_words)]
         before = " ".join(before_words)
         after = " ".join(after_words)
         html = ("%s %s %s" % (before, striptags(match), after)).strip()
-        kw_p = re.compile(r"(\S*%s\S*)" % keyword, re.IGNORECASE)
+        kw_p = re.compile(r"(\S*%s\S*)" % re.escape(keyword), re.IGNORECASE)
         html = kw_p.sub(r"<strong>\1</strong>", html)
 
         return mark_safe(html)
 
     return " ".join(clean_text(content)[:max_words])
diff --git a/tests/plugins/macros/test_toc.py b/tests/plugins/macros/test_toc.py
index 2fcaf0d8..328c129b 100644
--- a/tests/plugins/macros/test_toc.py
+++ b/tests/plugins/macros/test_toc.py
@@ -6,10 +6,12 @@ from wiki.plugins.macros.mdx.toc import WikiTocExtension
 from tests.base import RequireRootArticleMixin
 from tests.base import TestBase
 
 
 class TocMacroTests(TestCase):
+    maxDiff = None
+
     def test_toc_renders_table_of_content(self):
         """Verifies that the [TOC] wiki code renders a Table of Content"""
         md = Markdown(extensions=["extra", WikiTocExtension()])
         text = (
             "[TOC]\n"

Vulnerable code:         r"""
(?P<fence>^(?:~{3,}|`{3,}))[ ]*         # Opening ``` or ~~~
(\{?\.?(?P<lang>[a-zA-Z0-9_+-]*))?[ ]*  # Optional {, and lang
# Optional highlight lines, single- or double-quote-delimited
(hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot))?[ ]*
}?[ ]*\n                                # Optional closing }
(?P<code>.*?)(?<=\n)
(?P=fence)[ ]*$""",
MACRO_RE = r"(\[(?P<macro>\w+)(?P<kwargs>\s\w+\:.+)*\])"
        kw_p = re.compile(r"(\S*%s\S*)" % keyword, re.IGNORECASE)
Fixed code: 0.10.1
------

Released on 2024-03-16

Security
~~~~~~~~

* Fixes reDOS issues: Denial of Service possible through unsafe regular expressions `GHSA-wj85-w4f4-xh8h <https://github.com/django-wiki/django-wiki/security/advisories/GHSA-wj85-w4f4-xh8h>`__ (Santos Gallegos, Benjamin Balder Bach)


from textwrap import dedent


        dedent(
            r"""
            (?P<fence>^(?:~{3,}|`{3,}))[ ]*                          # opening fence
            ((\{(?P<attrs>[^\}\n]*)\})|                              # (optional {attrs} or
            (\.?(?P<lang>[\w#.+-]*)[ ]*)?                            # optional (.)lang
            (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot)[ ]*)?) # optional hl_lines)
            \n                                                       # newline (end of opening fence)
            (?P<code>.*?)(?<=\n)                                     # the code block
            (?P=fence)[ ]*$                                          # closing fence
        """
        ),

MACRO_RE = (
    r"""\[(?P<macro>\w+)(?P<kwargs>(\s+\w+\:([^\:\]\s]+|'[^']+'))+)*\]"""
)



        kw_p = re.compile(r"(\S*%s\S*)" % re.escape(keyword), re.IGNORECASE)
    maxDiff = None