{"id":"CVE-2026-113652","aliases":["GHSA-hhph-8536-gfq5"],"title":"ApostropheCMS uploadfs Azure backend leaks account-level SAS token into every public attachment URL (sas: true)","summary":"ApostropheCMS uploadfs Azure backend leaks account-level SAS token into every public attachment URL (sas: true)","severity":"medium","cvss":5.6,"cwe":["CWE-200","CWE-522"],"vendor":"uploadfs","product":"uploadfs","ecosystem":"npm","affected":["uploadfs >= 1.25.0, < 1.28.0"],"patched":["uploadfs 1.28.0"],"published":"2026-10-20","updated":"2026-10-20","sourceUpdated":"2026-10-20T18:20:36Z","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-hhph-8536-gfq5","references":[{"url":"https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-hhph-8536-gfq5"},{"url":"https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce"},{"url":"https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350"},{"url":"https://github.com/apostrophecms/apostrophe/releases/tag/uploadfs@1.28.0"},{"url":"https://github.com/advisories/GHSA-hhph-8536-gfq5"}],"tags":["ghsa","npm","cve.org"],"ingestedAt":"2026-10-20T17:12:53.601Z","slug":"CVE-2026-113652","body":"## Overview\n\n## 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://<account>.blob.core.windows.net?<SAS query string>\")` 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).\n\n## Affected packages\n\n- `uploadfs >= 1.25.0, < 1.28.0`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `uploadfs 1.28.0`","depth":"sunlit","depthScore":31,"depthScoreParts":{"impact":30.8,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}