Skip to content

fix(uploads): close multipart storage-quota bypass via quota-exempt contexts#5155

Merged
waleedlatif1 merged 1 commit into
stagingfrom
worktree-fix+multipart-quota-bypass
Jun 20, 2026
Merged

fix(uploads): close multipart storage-quota bypass via quota-exempt contexts#5155
waleedlatif1 merged 1 commit into
stagingfrom
worktree-fix+multipart-quota-bypass

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • The multipart upload endpoint (POST /api/files/multipart) accepted the quota-exempt public-asset contexts og-images, profile-pictures, and workspace-logos, which skip checkStorageQuota. Any authenticated writer — including a free-tier account — could open arbitrarily large upload sessions (100 GB+ confirmed) that never counted against their plan limit, causing unbounded untracked storage cost (CWE-770).
  • Fix: remove those three contexts from ALLOWED_UPLOAD_CONTEXTS (joining logs), so every context the multipart endpoint serves is quota-enforced. The bypass is now structurally impossible.

Why this is the right (and backwards-compatible) fix

  • These contexts have no legitimate multipart flow: their client hooks hard-cap uploads at 5 MB (image-only), and the direct-upload strategy only switches to multipart above 50 MB — so they always route through the single-part presigned endpoint (which already enforces admin/image-type policy).
  • og-images has neither a client nor a server multipart write path.
  • No change to workspace / mothership / knowledge-base / chat / copilot / execution behavior; workspace/mothership keep their existing MAX_WORKSPACE_FILE_SIZE cap.

Type of Change

  • Bug fix (security)

Testing

  • Updated route.test.ts: every quota-exempt context (og-images, profile-pictures, workspace-logos, logs) now returns 400 Invalid storage context before any session/quota call; quota-enforced contexts still pass the quota check and initiate.
  • bunx tsc --noEmit — 0 errors; vitest multipart suite — 11/11 pass; bun run check:api-validation — passes.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…ontexts

The multipart endpoint accepted the quota-exempt public-asset contexts
(og-images, profile-pictures, workspace-logos), which skip checkStorageQuota,
letting any authenticated writer open arbitrarily large upload sessions that
never count against their plan limit.

These contexts have no large-file flow: their client hooks hard-cap uploads at
5MB (image-only) and the direct-upload strategy only uses multipart above 50MB,
so they always route through the presigned endpoint. Remove them from
ALLOWED_UPLOAD_CONTEXTS (joining logs) so every context the multipart endpoint
serves is quota-enforced.
@vercel

vercel Bot commented Jun 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 20, 2026 8:57pm

Request Review

@cursor

cursor Bot commented Jun 20, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Security fix on billing/storage enforcement; scope is limited to multipart allowlisting and tests, with no change to legitimate small-asset presigned flows.

Overview
Closes a storage-quota bypass on POST /api/files/multipart where og-images, profile-pictures, and workspace-logos were allowed to initiate multipart uploads while skipping checkStorageQuota, enabling very large uploads that did not count against plan limits.

Those three contexts are removed from ALLOWED_UPLOAD_CONTEXTS, alongside the existing logs exclusion, so multipart initiate only accepts quota-enforced contexts (knowledge-base, chat, copilot, mothership, execution, workspace). Comments in the route and QUOTA_EXEMPT_STORAGE_CONTEXTS document that quota-exempt assets stay on presigned/single-part paths only.

Initiate quota logic now keys off the validated storageContext after the allowlist check. Tests assert quota-enforced success paths and use it.each to reject all four quota-exempt contexts with 400 Invalid storage context before quota or S3 initiate run.

Reviewed by Cursor Bugbot for commit e8ffb15. Configure here.

@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR closes a storage-quota bypass in the multipart upload endpoint by removing the three quota-exempt public-asset contexts (profile-pictures, workspace-logos, og-images) from ALLOWED_UPLOAD_CONTEXTS, joining the already-excluded logs context. A minor correctness cleanup also swaps a redundant context as StorageContext cast for the already-typed storageContext local in the quota guard.

  • Security fix: quota-exempt contexts are now structurally unreachable at the multipart endpoint — the ALLOWED_UPLOAD_CONTEXTS and QUOTA_EXEMPT_STORAGE_CONTEXTS sets are now fully disjoint, making the bypass impossible without a code change.
  • Tests updated: parameterized it.each covers all four excluded contexts, and the "passes quota" path is verified end-to-end with mockInitiateS3MultipartUpload now properly wired into the S3 client mock.
  • Docs updated: the JSDoc on QUOTA_EXEMPT_STORAGE_CONTEXTS in types.ts is corrected to describe the new invariant (all exempt contexts excluded from multipart, not just logs).

Confidence Score: 5/5

Safe to merge — the change is a minimal, well-scoped removal of three context strings that had no legitimate multipart flow; it cannot regress existing quota-enforced uploads.

The fix is structurally sound: ALLOWED_UPLOAD_CONTEXTS and QUOTA_EXEMPT_STORAGE_CONTEXTS are now fully disjoint, so every context the multipart endpoint accepts is quota-checked. The quota guard itself was also cleaned up to use the already-cast local. Tests cover all four excluded contexts with a parameterized suite and verify the happy path end-to-end. No pre-existing quota-enforced contexts were removed or altered.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/app/api/files/multipart/route.ts Removes the three quota-exempt contexts from ALLOWED_UPLOAD_CONTEXTS and fixes a minor cast redundancy in the quota guard; logic is correct and the sets are now fully disjoint.
apps/sim/lib/uploads/shared/types.ts JSDoc on QUOTA_EXEMPT_STORAGE_CONTEXTS updated to reflect the new invariant; no behavioral changes to the type definitions or the set itself.
apps/sim/app/api/files/multipart/route.test.ts Tests are updated to cover all four excluded quota-exempt contexts via it.each and the happy-path now asserts both quota check and S3 initiation; mockInitiateS3MultipartUpload is properly wired into the S3 client mock.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["POST /api/files/multipart?action=initiate"] --> B{Authenticated?}
    B -- No --> C[401 Unauthorized]
    B -- Yes --> D{Cloud storage enabled?}
    D -- No --> E[400 Cloud storage only]
    D -- Yes --> F{context in ALLOWED_UPLOAD_CONTEXTS?}
    
    F -- "No\n'og-images','profile-pictures',\n'workspace-logos','logs'" --> G["400 Invalid storage context\n(quota bypass blocked)"]
    
    F -- "Yes\n'knowledge-base','chat','copilot',\n'mothership','execution','workspace'" --> H{User has workspace write/admin?}
    H -- No --> I[403 Forbidden]
    H -- Yes --> J{context in QUOTA_EXEMPT_STORAGE_CONTEXTS?}
    
    J -- "Yes (dead path after fix)" --> K[Skip quota check]
    J -- "No (always taken now)" --> L["checkStorageQuota(userId, fileSize)"]
    L -- Denied --> M[413 Storage limit exceeded]
    L -- Allowed --> N[Initiate S3/Blob multipart upload]
    K --> N
    N --> O[Return uploadId + key + uploadToken]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["POST /api/files/multipart?action=initiate"] --> B{Authenticated?}
    B -- No --> C[401 Unauthorized]
    B -- Yes --> D{Cloud storage enabled?}
    D -- No --> E[400 Cloud storage only]
    D -- Yes --> F{context in ALLOWED_UPLOAD_CONTEXTS?}
    
    F -- "No\n'og-images','profile-pictures',\n'workspace-logos','logs'" --> G["400 Invalid storage context\n(quota bypass blocked)"]
    
    F -- "Yes\n'knowledge-base','chat','copilot',\n'mothership','execution','workspace'" --> H{User has workspace write/admin?}
    H -- No --> I[403 Forbidden]
    H -- Yes --> J{context in QUOTA_EXEMPT_STORAGE_CONTEXTS?}
    
    J -- "Yes (dead path after fix)" --> K[Skip quota check]
    J -- "No (always taken now)" --> L["checkStorageQuota(userId, fileSize)"]
    L -- Denied --> M[413 Storage limit exceeded]
    L -- Allowed --> N[Initiate S3/Blob multipart upload]
    K --> N
    N --> O[Return uploadId + key + uploadToken]
Loading

Reviews (2): Last reviewed commit: "fix(uploads): close multipart storage-qu..." | Re-trigger Greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e8ffb15. Configure here.

@waleedlatif1 waleedlatif1 merged commit 2f7d607 into staging Jun 20, 2026
16 checks passed
@waleedlatif1 waleedlatif1 deleted the worktree-fix+multipart-quota-bypass branch June 20, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant