{"id":"GHSA-hhph-8536-gfq5","summary":"ApostropheCMS uploadfs Azure backend leaks account-level SAS token into every public attachment URL (sas: true)","details":"## Summary\n\nWhen uploadfs is configured with the Azure backend and `sas: true` (a supported configuration introduced in uploadfs 1.25.0), `createContainer()` builds the `BlobServiceClient` from a URL that embeds the full SAS query string, and Azure SDK v12 derived clients inherit that query string. As a result, `getUrl()` returns a base URL carrying the complete account/container SAS signature (`?sv=...&sig=...`), and Apostrophe's `attachment.url()` prepends it to every public attachment link — broadcasting a container-level SAS credential (commonly `sp=rwdlac`, full read/write/delete/list) to every site visitor, including anonymous ones. Anyone who can see an attachment URL can read, overwrite, delete, and list every blob in the container for the SAS lifetime. A companion functional bug (blob path appended after the query string, producing broken URLs) shows this code path was never exercised end to end. Shared-key mode (default) and non-Azure backends are not affected.\n\nVerified at commit `5a3746aaed49761e171c2cbfe793267c959829fd` (uploadfs 1.27.0 in the apostrophe monorepo).\n\n## Details\n\nRoot cause: a credential-bearing client URL is reused as the public base URL. The chain:\n\n- Credential-embedded client: [azure.js#L123-L125](https://github.com/apostrophecms/apostrophe/blob/5a3746aaed49761e171c2cbfe793267c959829fd/packages/uploadfs/lib/storage/azure.js#L123-L125) — `new BlobServiceClient(\"https://\u003caccount\u003e.blob.core.windows.net?\u003cSAS query string\u003e\")` where `cluster.key` is the full SAS string including `sig`\n- Leak point: [azure.js#L513-L520](https://github.com/apostrophecms/apostrophe/blob/5a3746aaed49761e171c2cbfe793267c959829fd/packages/uploadfs/lib/storage/azure.js#L513-L520) — `getUrl()` takes `getContainerClient(...).getBlobClient('').url` as `baseUrl`; SDK v12 derived clients inherit the SAS query string, so `baseUrl` already contains `?sv=...&sig=...`\n- Public exposure: [attachment/index.js#L694](https://github.com/apostrophecms/apostrophe/blob/5a3746aaed49761e171c2cbfe793267c959829fd/packages/apostrophe/modules/@apostrophecms/attachment/index.js#L694) — `attachment.url()` = `uploadfs.getUrl() + path` for every rendered attachment link; [attachment/index.js#L1503](https://github.com/apostrophecms/apostrophe/blob/5a3746aaed49761e171c2cbfe793267c959829fd/packages/apostrophe/modules/@apostrophecms/attachment/index.js#L1503) — `getBrowserData()` also ships `uploadsUrl` to every logged-in editor session\n- Companion bug: the blob path is appended **after** the query string (`/container/?sv=...&sig=XXX/attachments/id-name`), so these URLs are also broken — evidence the 1.25.0 code path was never tested\n\nAttack surface: every public page rendering attachment URLs (images, download links), plus the `uploadsUrl` delivered to editor sessions. The leak is passive and unconditional — no action required from the victim beyond serving a page. The credential further propagates through HTML source, logs, Referer headers, and CDN/proxy access logs.\n\n**Suggested remediation:**\n1. Strip the query string before building public URLs: `const u = new URL(blob.svc.getContainerClient(...).getBlobClient('').url); u.search = '';` (or compose `accountUrl + '/' + container` manually), and append the blob path to the pathname, not after the query.\n2. Document that SAS mode must use least privilege (read-only, ideally `sr=b` per-blob), and prefer generating short-lived per-blob read-only signed URLs via `generateBlobSASQueryParameters` instead of embedding a service-level SAS in permanent public URLs.\n\n## PoC\n\nFull PoC posted as the first comment below (`poc.js` + README; offline, no Azure account needed). It replicates the library's exact two steps with the declared dependency `@azure/storage-blob@12`:\n\n1. `npm install @azure/storage-blob@12 && node poc.js`\n2. The script constructs `new BlobServiceClient(\"https://myaccount.blob.core.windows.net?sv=...&sig=FAKESIGNATURE\")` exactly like `createContainer()`, then evaluates the `getUrl()` logic (`getContainerClient('uploads-container').getBlobClient('').url`) and asserts the resulting base URL still contains the `sig` parameter.\n3. Observed output (exit code 0):\n\n```\nbaseUrl: https://myaccount.blob.core.windows.net/uploads-container/?sv=2021-06-08&ss=b&srt=co&sp=rwdlac&se=2099-01-01&sig=FAKESIGNATURE\nattachment URL as served to visitors: https://myaccount.blob.core.windows.net/uploads-container/?sv=2021-06-08&ss=b&srt=co&sp=rwdlac&se=2099-01-01&sig=FAKESIGNATURE/attachments/abc123-photo.jpg\n```\n\nThe produced URL both leaks the token (`sv`…`sig`) and demonstrates the broken path placement. Cross-checking `attachment/index.js` confirms this URL reaches public pages unmodified.\n\n## Impact\n\n- **What it is:** insufficiently protected credentials (CWE-522) leading to information exposure (CWE-200). A container-level SAS token — commonly granted `sp=rwdlac` (read/write/delete/list/ACL) — is disclosed to every site visitor including anonymous users, valid for the SAS lifetime (often years). Holders can read, overwrite, delete, and enumerate every attachment in the container; the library applies no narrowing.\n- **Who is impacted:** Apostrophe sites using uploadfs with the Azure backend and `sas: true` (introduced in 1.25.0; present through 1.27.0). Default shared-key mode and other backends are unaffected. When the SAS is provisioned read-only, the impact reduces to unauthenticated read/download of all attachments.\n\n---\n\nReported by **Kimi Security Team** (bug-report@moonshot.ai).","aliases":["CVE-2026-113652"],"modified":"2026-10-20T18:30:03.590258002Z","published":"2026-10-20T18:20:35Z","database_specific":{"github_reviewed_at":"2026-10-20T18:20:35Z","nvd_published_at":null,"cwe_ids":["CWE-200","CWE-522"],"severity":"MODERATE","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-hhph-8536-gfq5"},{"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/uploadfs@1.28.0"}],"affected":[{"package":{"name":"uploadfs","ecosystem":"npm","purl":"pkg:npm/uploadfs"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.25.0"},{"fixed":"1.28.0"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-hhph-8536-gfq5/GHSA-hhph-8536-gfq5.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L"}]}