{"id":"CVE-2026-45013","aliases":["EUVD-2026-36571","GHSA-gf43-24g3-5hw2"],"status":"published","title":"Apostrophe has a Weak Password Recovery Mechanism for Forgotten Password and Improper Input Validation","description":"ApostropheCMS is an open-source Node.js content management system. Versions up to and including 4.29.0 have a password reset flow that 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. As of time of publication, no known patched versions are available.","severity":{"version":"3.1","score":8.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00305,"percentile":0.23425,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":false},"cwes":["CWE-20","CWE-640"],"published":"2026-05-14T18:27:12Z","modified":"2026-10-28T02:31:03Z","affected":[{"source":"ghsa","ecosystem":"npm","package":"apostrophe","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"4.29.0"}]}]},{"source":"osv","ecosystem":"npm","package":"apostrophe","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"last_affected":"4.29.0"}]}]}],"products":[{"vendor":"apostrophecms","product":"apostrophe"}],"references":[{"url":"https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-gf43-24g3-5hw2","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-45013","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-gf43-24g3-5hw2"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/45xxx/CVE-2026-45013.json","tags":["advisory"]},{"url":"https://github.com/apostrophecms/apostrophe","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-45013","title":"Apostrophe has a Weak Password Recovery Mechanism for Forgotten Password and Improper Input Validation","description":"ApostropheCMS is an open-source Node.js content management system. Versions up to and including 4.29.0 have a password reset flow that 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. As of time of publication, no known patched versions are available.","status":"published","published":"2026-06-12T20:46:21.628Z","modified":"2026-06-13T03:37:07.486Z","cvss":[{"version":"3.1","score":8.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N","level":"high","assessor":"cna"}],"cwes":["CWE-20","CWE-640"],"updated_at":"2026-11-02T13:46:32.755125Z"}],"euvd":[{"id":"EUVD-2026-36571","description":"ApostropheCMS is an open-source Node.js content management system. Versions up to and including 4.29.0 have a password reset flow that 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. As of time of publication, no known patched versions are available.","status":"published","published":"2026-06-12T20:46:21Z","modified":"2026-06-13T03:37:07Z","cvss":[{"version":"3.1","score":8.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N","level":"high"}],"updated_at":"2026-11-02T18:20:43.222Z"}],"ghsa":[{"id":"GHSA-gf43-24g3-5hw2","title":"Apostrophe has a Weak Password Recovery Mechanism for Forgotten Password and Improper Input Validation","description":"## Summary\n\nApostropheCMS's password reset flow constructs the reset URL using `req.hostname`, \nwhich is derived directly from the attacker-controlled HTTP `Host` header when \n`apos.baseUrl` is not explicitly configured. An unauthenticated attacker who knows \na victim's email address can send a crafted reset request that causes the application \nto email the victim a reset link pointing to the attacker's domain. When the victim \nclicks the link, the valid reset token is delivered to the attacker, enabling full \naccount takeover.\n\n## Affected Component\n\n`modules/@apostrophecms/login/index.js` — `resetRequest` route  \nPrecondition: `passwordReset: true` is set **and** `apos.baseUrl` is not configured.\n\n## Vulnerability Details\n\nThe `setPrefixUrls` middleware (i18n layer) builds `req.baseUrl` using `req.hostname`:\n\n```js\n// Simplified from i18n middleware\nreq.baseUrl = `${req.protocol}://${req.hostname}`;\nreq.absoluteUrl = req.baseUrl + req.url;\n```\n\nThe `resetRequest` handler then passes this tainted value directly into URL construction:\n\n```js\nconst parsed = new URL(\n  req.absoluteUrl,           // ← tainted by attacker's Host header\n  self.apos.baseUrl\n    ? undefined\n    : `${req.protocol}://${req.hostname}${port}`  // ← also tainted\n);\nparsed.pathname = '/login';\nparsed.searchParams.append('reset', reset);   // real, valid token\nparsed.searchParams.append('email', user.email);\nawait self.email(..., { url: parsed.toString() }, ...);\n// Email sent to victim with URL pointing to attacker-controlled domain\n```\n\nWhen `apos.baseUrl` is configured, it is used unconditionally and the attacker's \n`Host` header is ignored — that path is **not** vulnerable.\n\n## Attack Scenario\n\n1. Attacker identifies a valid user email (e.g. from the site's public interface).\n2. Attacker sends:\n```\n   POST /api/v1/login/reset-request\n   Host: evil.attacker.com\n   Content-Type: application/json\n\n   {\"email\": \"victim@example.com\"}\n```\n3. The application emails the victim:\n```\n   Click here to reset your password:\n   http://evil.attacker.com/login?reset=TOKEN\u0026email=victim@example.com\n```\n4. Victim clicks the link; attacker's server captures `TOKEN`.\n5. Attacker calls the real target's reset endpoint with the captured token and \n   sets a new password — full account takeover.\n\n## Preconditions\n\n- `passwordReset: true` configured in login module options (opt-in)\n- `apos.baseUrl` is **not** set (common in development and some production deployments)\n- Attacker knows or can enumerate a valid account email\n\n## Impact\n\nFull account takeover of any account whose email address is known to the attacker. \nNo authentication or interaction beyond sending a single HTTP request is required \nfrom the attacker. The victim need only click a link in a legitimate-looking \npassword reset email from their own site.\n\n## Remediation\n\n**Operators (immediate):** Always set `apos.baseUrl` in your configuration:\n\n```js\n// app.js or module configuration\nmodules: {\n  '@apostrophecms/express': {\n    options: {\n      baseUrl: 'https://yourdomain.com'\n    }\n  }\n}\n```\n\n**Framework fix (recommended):** The `resetRequest` route should refuse to proceed \nif `apos.baseUrl` is not configured, rather than falling back to the tainted \n`req.hostname`. Example:\n\n```js\n// In resetRequest handler\nif (!self.apos.baseUrl) {\n  throw self.apos.error(\n    'invalid',\n    'apos.baseUrl must be configured to enable password reset'\n  );\n}\nconst parsed = new URL(self.loginUrl(), self.apos.baseUrl);\n```\n\nThis eliminates the attacker-controlled input entirely from the URL construction path.\n\n## References\n\n- [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)\n- [CWE-640: Weak Password Recovery Mechanism for Forgotten Password](https://cwe.mitre.org/data/definitions/640.html)","status":"published","published":"2026-05-14T18:27:12Z","modified":"2026-06-12T22:02:14Z","cvss":[{"version":"3.1","score":8.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-20","CWE-640"],"updated_at":"2026-11-02T14:40:27.329706Z"}],"nvd":[{"id":"CVE-2026-45013","description":"ApostropheCMS is an open-source Node.js content management system. Versions up to and including 4.29.0 have a password reset flow that 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. As of time of publication, no known patched versions are available.","status":"published","published":"2026-06-12T21:16:22.85Z","modified":"2026-10-25T23:10:00.167Z","cvss":[{"version":"3.1","score":8.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N","level":"high","assessor":"secondary"}],"cwes":["CWE-20","CWE-640"],"updated_at":"2026-11-02T14:12:23.846948Z"}],"osv":[{"id":"CVE-2026-45013","title":"Apostrophe has a Weak Password Recovery Mechanism for Forgotten Password and Improper Input Validation","description":"ApostropheCMS is an open-source Node.js content management system. Versions up to and including 4.29.0 have a password reset flow that 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. As of time of publication, no known patched versions are available.","status":"published","published":"2026-06-12T20:46:21.628Z","modified":"2026-10-28T02:31:03.662971058Z","cvss":[{"version":"3.1","score":8.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-20","CWE-640"],"updated_at":"2026-11-02T15:45:05.172514Z"},{"id":"GHSA-gf43-24g3-5hw2","title":"Apostrophe has a Weak Password Recovery Mechanism for Forgotten Password and Improper Input Validation","description":"## Summary\n\nApostropheCMS's password reset flow constructs the reset URL using `req.hostname`, \nwhich is derived directly from the attacker-controlled HTTP `Host` header when \n`apos.baseUrl` is not explicitly configured. An unauthenticated attacker who knows \na victim's email address can send a crafted reset request that causes the application \nto email the victim a reset link pointing to the attacker's domain. When the victim \nclicks the link, the valid reset token is delivered to the attacker, enabling full \naccount takeover.\n\n## Affected Component\n\n`modules/@apostrophecms/login/index.js` — `resetRequest` route  \nPrecondition: `passwordReset: true` is set **and** `apos.baseUrl` is not configured.\n\n## Vulnerability Details\n\nThe `setPrefixUrls` middleware (i18n layer) builds `req.baseUrl` using `req.hostname`:\n\n```js\n// Simplified from i18n middleware\nreq.baseUrl = `${req.protocol}://${req.hostname}`;\nreq.absoluteUrl = req.baseUrl + req.url;\n```\n\nThe `resetRequest` handler then passes this tainted value directly into URL construction:\n\n```js\nconst parsed = new URL(\n  req.absoluteUrl,           // ← tainted by attacker's Host header\n  self.apos.baseUrl\n    ? undefined\n    : `${req.protocol}://${req.hostname}${port}`  // ← also tainted\n);\nparsed.pathname = '/login';\nparsed.searchParams.append('reset', reset);   // real, valid token\nparsed.searchParams.append('email', user.email);\nawait self.email(..., { url: parsed.toString() }, ...);\n// Email sent to victim with URL pointing to attacker-controlled domain\n```\n\nWhen `apos.baseUrl` is configured, it is used unconditionally and the attacker's \n`Host` header is ignored — that path is **not** vulnerable.\n\n## Attack Scenario\n\n1. Attacker identifies a valid user email (e.g. from the site's public interface).\n2. Attacker sends:\n```\n   POST /api/v1/login/reset-request\n   Host: evil.attacker.com\n   Content-Type: application/json\n\n   {\"email\": \"victim@example.com\"}\n```\n3. The application emails the victim:\n```\n   Click here to reset your password:\n   http://evil.attacker.com/login?reset=TOKEN\u0026email=victim@example.com\n```\n4. Victim clicks the link; attacker's server captures `TOKEN`.\n5. Attacker calls the real target's reset endpoint with the captured token and \n   sets a new password — full account takeover.\n\n## Preconditions\n\n- `passwordReset: true` configured in login module options (opt-in)\n- `apos.baseUrl` is **not** set (common in development and some production deployments)\n- Attacker knows or can enumerate a valid account email\n\n## Impact\n\nFull account takeover of any account whose email address is known to the attacker. \nNo authentication or interaction beyond sending a single HTTP request is required \nfrom the attacker. The victim need only click a link in a legitimate-looking \npassword reset email from their own site.\n\n## Remediation\n\n**Operators (immediate):** Always set `apos.baseUrl` in your configuration:\n\n```js\n// app.js or module configuration\nmodules: {\n  '@apostrophecms/express': {\n    options: {\n      baseUrl: 'https://yourdomain.com'\n    }\n  }\n}\n```\n\n**Framework fix (recommended):** The `resetRequest` route should refuse to proceed \nif `apos.baseUrl` is not configured, rather than falling back to the tainted \n`req.hostname`. Example:\n\n```js\n// In resetRequest handler\nif (!self.apos.baseUrl) {\n  throw self.apos.error(\n    'invalid',\n    'apos.baseUrl must be configured to enable password reset'\n  );\n}\nconst parsed = new URL(self.loginUrl(), self.apos.baseUrl);\n```\n\nThis eliminates the attacker-controlled input entirely from the URL construction path.\n\n## References\n\n- [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)\n- [CWE-640: Weak Password Recovery Mechanism for Forgotten Password](https://cwe.mitre.org/data/definitions/640.html)","status":"published","published":"2026-05-14T18:27:12Z","modified":"2026-06-12T22:15:11.195098701Z","cvss":[{"version":"3.1","score":8.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-20","CWE-640"],"updated_at":"2026-11-02T15:46:47.822182Z"}]}}
