{"id":"CVE-2026-45013","aliases":["GHSA-gf43-24g3-5hw2"],"url":"https://o3.security/vulnerability/CVE-2026-45013","summary":"Apostrophe has a Weak Password Recovery Mechanism for Forgotten Password and Improper Input Validation","details":"## 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&email=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)","published":"2026-06-12T20:46:21.628Z","modified":"2026-08-12T03:51:34.196004953Z","cvss":{"score":8.1,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N"},"epss":{"score":0.0025,"percentile":0.16539,"asOf":"2026-08-14"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"apostrophe","fixedVersion":null}],"fix":null,"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/45xxx/CVE-2026-45013.json"},{"type":"ADVISORY","url":"https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-gf43-24g3-5hw2"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-45013"},{"type":"PACKAGE","url":"https://github.com/apostrophecms/apostrophe"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:34.196004953Z"}}