{"id":"CVE-2026-113668","kind":"vulnerability","aliases":["CVE-2026-113668","GHSA-gqh3-7856-rjjg"],"related":[],"title":"ApostropheCMS: :_id/locales REST route omits the public-API authorization gate its sibling read routes enforce (unauthenticated doc-existence + locale metadata disclosure)","description":"## Summary\nThe REST route `GET /api/v1/<piece-type>/:_id/locales` (and the equivalent page route) does not apply the `publicApiCheckAsync` gate that ApostropheCMS's other read routes enforce. An unauthenticated caller can therefore query this route for any document `_id` and learn whether the document exists and which locales it is published in — a document-existence and locale-metadata disclosure that the read-API is otherwise supposed to gate.\n \n## Affected\n- Repository: `apostrophecms/apostrophe` (monorepo package `apostrophe`).\n- Confirmed at **4.32.2** (commit `d3ff8c9` == the shipped 4.32.2; unchanged at current HEAD). No fix at time of report.\n- Stack: Node.js / Express + MongoDB.\n- Endpoints: `GET /api/v1/<piece-type>/:_id/locales` and `GET /api/v1/@apostrophecms/page/:_id/locales`.\n\n## Technical detail\n- Unsafe: `modules/@apostrophecms/piece-type/index.js:397` registers the `:_id/locales` route with **no** `publicApiCheckAsync`. The page module has the same omission at `modules/@apostrophecms/page/index.js:925`.\n- Gated siblings: the read routes in the same module (e.g. `getAll` at `piece-type/index.js:256-263`) call `publicApiCheckAsync` (`:263`) to enforce the public-API access policy.\n- So the `locales` handler (which calls `getLocales()` for the document) runs for any caller regardless of the public-API gate the other read routes apply, returning the document's locale/publication metadata without authorization.\n\n## Impact\n\nAn unauthenticated caller can confirm the existence of a document by `_id` and enumerate its locales/publication state — information the read-API is meant to gate. Defensibly 4.3 (AC:H) given the opaque `_id` precondition (the caller needs a document id).\n \n## How it was verified (local only)\nReproduced against `apostrophe@4.32.2` (== commit `d3ff8c9`) locally with synthetic content:\n- Source review confirmed the `:_id/locales` route lacks `publicApiCheckAsync` while the sibling read routes have it.\n- An **unauthenticated** `GET /api/v1/<piece-type>/<_id>/locales` returned the document's locale metadata (existence + locales), while the gated read routes (e.g. `getAll`) correctly required the public-API policy.\n- Control: the same route with the `publicApiCheckAsync` gate added (matching the siblings) denies the unauthenticated request — isolating the missing gate as the cause.\nSynthetic content only; nothing hosted was touched.\n \n## Remediation\nAdd the `publicApiCheckAsync` (or equivalent) authorization gate to the `:_id/locales` route in both the piece-type and page modules, matching the sibling read routes, so the locale/existence metadata is only returned to callers permitted by the public-API policy.\n \n## Credit\nPlease credit **Santosh Kumar Puppala** (https://github.com/Santoshkumarpuppala).","summary":"The ApostropheCMS REST route GET /api/v1/<piece-type>/:_id/locales (and the page equivalent) skips the public-API authorization check that sibling read routes apply. An unauthenticated caller who knows a document _id can learn whether the document exists and which locales it is published in.","severity":"medium","cvss":5.300000190734863,"epss":0.003220000071451068,"exploited":false,"exploited_at":null,"products":[{"name":"apostrophe","fixed":["4.33.0"],"affected":"< 4.33.0","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["apostrophe","cwe-200","cwe-862","headless-cms","node.js","package","self-hosted"],"not_affected_if":["no-public-api-exposure","version-4.33.0-or-later"],"how_to_check":"Check the installed apostrophe version with: npm ls apostrophe\nIf below 4.33.0, send an unauthenticated GET to /api/v1/<piece-type>/<known-_id>/locales on a site with public-API access restricted and see if it returns locale data.","fix":"Upgrade the apostrophe npm package to 4.33.0 or later.\nUntil then, unknown whether a configuration workaround exists.","status":"open","published_at":"2026-10-20T18:14:32+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T19:58:22+00:00","updated_at":"2026-10-23T19:58:22+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-23T19:58:22Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade the apostrophe npm package to 4.33.0 or later.\nUntil then, unknown whether a configuration workaround exists.","summary":"The ApostropheCMS REST route GET /api/v1/<piece-type>/:_id/locales (and the page equivalent) skips the public-API authorization check that sibling read routes apply. An unauthenticated caller who knows a document _id can learn whether the document exists and which locales it is published in.","category":"package","who_cares":["apostrophe","node.js","self-hosted","headless-cms"],"confidence":0.8,"analyst_note":"","how_to_check":"Check the installed apostrophe version with: npm ls apostrophe\nIf below 4.33.0, send an unauthenticated GET to /api/v1/<piece-type>/<known-_id>/locales on a site with public-API access restricted and see if it returns locale data.","needs_analyst":false,"not_affected_if":["version-4.33.0-or-later","no-public-api-exposure"],"severity_if_unknown":"medium"},"cost_usd":0.000487,"input_tokens":3071,"output_tokens":360,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-113668':26A,34A '-200':38B '-2026':25A,33A '-263':239C '-862':40B '/api/v1':54B,97C,191C,195C,361C '/index.js':237C '/locales':362C '/santoshkumarpuppala).':465C '256':238C '263':242C '397':203C '4.3':313C '4.32.2':169C,174C,336C '7856':30A '925':221C 'ac':314C 'access':249C 'add':393C,416C 'affect':161C 'api':9A,64B,155C,248C,270C,307C,383C,455C 'apostroph':36B,166C,335C 'apostrophe':35A 'apostrophecm':1A,50B,111C 'apostrophecms/apostrophe':163C 'apostrophecms/page':196C 'apostrophecms/page/index.js':220C 'apostrophecms/piece-type/index.js':202C 'appli':71B,106C,276C 'author':10A,65B,284C,421C 'call':240C,256C 'caller':74B,119C,264C,288C,322C,449C 'caus':407C 'check':66B 'cms':43B 'commit':170C,337C 'confirm':167C,290C,345C 'content':342C,409C 'control':385C 'correct':378C 'credit':457C,459C 'current':177C 'cve':24A,32A 'cwe':37B,39B 'd3ff8c9':171C,338C 'defens':312C 'deni':397C 'detail':199C 'disclosur':23A,150C 'doc':19A 'doc-exist':18A 'document':78B,84B,127C,133C,144C,260C,279C,295C,325C,365C 'document-exist':143C 'e.g':231C,376C 'endpoint':189C 'enforc':16A,116C,244C 'enumer':299C 'equival':59B,101C,420C 'exist':20A,85B,134C,145C,292C,369C 'express':187C 'fix':180C 'gate':11A,109C,160C,222C,271C,311C,373C,392C,404C,422C 'get':53B,96C,190C,194C,360C 'getal':232C,377C 'getlocal':257C 'ghsa':28A 'ghsa-gqh3':27A 'github.com':464C 'github.com/santoshkumarpuppala).':463C 'given':316C 'gqh3':29A 'h':315C 'handler':254C 'head':178C 'headless':42B 'headless-cm':41B 'host':48B,412C 'id':79B,128C,297C,319C,326C 'id/locales':2A,55B,98C,192C,197C,206C,347C,425C 'impact':285C 'inform':303C 'isol':401C 'know':76B 'kumar':461C 'lack':349C 'learn':81B,130C 'local':21A,88B,137C,148C,253C,331C,339C,367C,370C 'locale-metadata':147C 'locale/existence':443C 'locale/publication':281C 'locales/publication':301C 'match':394C,436C 'meant':309C 'metadata':22A,149C,282C,368C,444C 'miss':403C 'modul':201C,213C,219C,230C,435C 'mongodb':188C 'monorepo':164C 'need':323C 'node.js':44B,186C 'noth':411C 'omiss':217C 'omit':5A 'opaqu':318C 'otherwis':157C 'packag':45B,165C 'page':58B,102C,212C,434C 'permit':450C 'piec':235C,431C 'piece-typ':234C,430C 'pleas':458C 'polici':250C,384C,456C 'precondit':320C 'public':8A,63B,247C,269C,382C,454C 'public-api':7A,62B,246C,268C,381C,453C 'publicapicheckasync':108C,210C,241C,350C,391C,418C 'publish':91B,140C 'puppala':462C 'queri':122C 'read':14A,69B,114C,154C,225C,274C,306C,354C,374C,439C 'read-api':153C,305C 'regardless':265C 'regist':204C 'remedi':415C 'report':184C 'repositori':162C 'reproduc':333C 'request':400C 'requir':379C 'rest':3A,51B,94C 'return':277C,363C,447C 'review':344C 'rjjg':31A 'rout':4A,15A,52B,70B,95C,103C,115C,124C,207C,226C,275C,348C,355C,375C,388C,426C,440C 'run':261C 'santosh':460C 'self':47B 'self-host':46B 'ship':173C 'sibl':13A,68B,223C,353C,396C,438C 'skip':60B 'sourc':343C 'stack':185C 'state':302C 'summari':92C 'suppos':158C 'synthet':341C,408C 'technic':198C 'therefor':121C 'time':182C 'touch':414C 'type':236C,432C 'unauthent':17A,73B,118C,287C,359C,399C 'unchang':175C 'unsaf':200C 'verifi':330C 'whether':82B,131C 'without':283C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-20T18:14:32+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-gqh3-7856-rjjg","url":"https://github.com/advisories/GHSA-gqh3-7856-rjjg","severity":"medium","published_at":"2026-10-20T18:14:32+00:00","modified_at":"2026-10-20T18:14:33+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-113668","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-113668","severity":null,"published_at":"2026-10-20T18:17:20+00:00","modified_at":"2026-10-20T19:53:10+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"}