GHSA-hhph-8536-gfq5

Suggest an improvement
Source
https://github.com/advisories/GHSA-hhph-8536-gfq5
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-hhph-8536-gfq5/GHSA-hhph-8536-gfq5.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-hhph-8536-gfq5
Aliases
  • CVE-2026-113652
Published
2026-10-20T18:20:35Z
Modified
2026-10-20T18:30:03Z
Severity
  • 5.6 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L CVSS Calculator
Summary
ApostropheCMS uploadfs Azure backend leaks account-level SAS token into every public attachment URL (sas: true)
Details

Summary

When 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.

Verified at commit 5a3746aaed49761e171c2cbfe793267c959829fd (uploadfs 1.27.0 in the apostrophe monorepo).

Details

Root cause: a credential-bearing client URL is reused as the public base URL. The chain:

  • Credential-embedded client: 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
  • Leak point: 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=...
  • Public exposure: attachment/index.js#L694 — attachment.url() = uploadfs.getUrl() + path for every rendered attachment link; attachment/index.js#L1503 — getBrowserData() also ships uploadsUrl to every logged-in editor session
  • 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

Attack 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.

Suggested remediation:

  1. 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.
  2. 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.

PoC

Full 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:

  1. npm install @azure/storage-blob@12 && node poc.js
  2. 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.
  3. Observed output (exit code 0):
baseUrl: https://myaccount.blob.core.windows.net/uploads-container/?sv=2021-06-08&ss=b&srt=co&sp=rwdlac&se=2099-01-01&sig=FAKESIGNATURE
attachment 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

The 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.

Impact

  • 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.
  • 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.

Reported by Kimi Security Team (bug-report@moonshot.ai).

Database specific
{
    "cwe_ids":  [
        "CWE-200",
        "CWE-522"
    ],
    "github_reviewed":  true,
    "github_reviewed_at":  "2026-10-20T18:20:35Z",
    "nvd_published_at":  null,
    "severity":  "MODERATE"
}
References

Affected packages

npm / uploadfs

Package

Affected 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"