---
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'
---

## Overview

## 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](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`
- 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=...`
- 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
- 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).

## Affected packages

- `uploadfs >= 1.25.0, < 1.28.0`

## Remediation

Upgrade to a patched release:

- `uploadfs 1.28.0`
