feat(media): complete v3 plugin sweep - #147
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2796795. Configure here.
There was a problem hiding this comment.
Security Review — feat(media): complete v3 plugin sweep
Reviewed the full diff across 25 files. No high-confidence vulnerabilities found. Several security-relevant changes are noted below.
Positive security changes
Search query hardening (schemas.ts, getters.ts)
AssetListQuerySchemanow enforces.max(200)on thequeryfield — closes the unbounded input vector.- The in-memory fallback scan is capped at 1,000 rows (
limit: needsInMemoryFilter ? 1000 : query.limit), preventing a full-table load for large libraries. This is a conscious, bounded trade-off; the cap is documented inline. - Query is
.trim()-ed and coerced toundefinedwhen blank both in the schema and ingetters.ts, eliminating whitespace-only strings from DB comparisons.
URL registration validation unchanged (plugin.ts)
POST /media/assetsstill enforces the existing adapter-specific allowlist (allowedUrlPrefixes, S3 prefix match, Vercel Blob hostname suffix).localAdaptercontinues to reject all client-supplied URLs. The newuseRegisterAssetFormhook on the client side does not bypass or weaken these server-side checks.
FolderListQuerySchema sentinel (schemas.ts)
- The
__root__sentinel is sanitised by Zod's.transform()before reaching thelistFoldersgetter and theonBeforeListFoldershook. The hook's updated signature{ parentId?: string | null }correctly reflects the post-transform value, so no information is hidden from authorization hooks.
Client-side useCan() checks (multiple components)
- Upload, URL registration, delete, and folder creation controls are hidden/disabled when the permission check returns false. These are presentation-layer guards, correctly documented as such: the authoritative boundary remains the backend
onBefore*hooks. No authz bypass is introduced.
Items warranting attention (not blocking, but worth tracking)
filenameFromUrl decodes URL path segments before sending to the server (use-media.tsx)
function filenameFromUrl(url: string): string {
try {
const filename = new URL(url).pathname.split("/").filter(Boolean).pop();
return filename ? decodeURIComponent(filename) : "asset";
} catch {
return "asset";
}
}If the last path segment contains a percent-encoded slash (%2F), decodeURIComponent will produce a literal / in the filename stored in the DB (e.g. GET https://cdn.example.com/images%2Fphoto.jpg → filename: "images/photo.jpg"). This is metadata pollution rather than a filesystem risk (the server never uses filename for I/O in the asset-registration path, and localAdapter rejects client URLs entirely). However, if the stored filename is later served in a Content-Disposition header without sanitization, a crafted value like foo%2Fetc%2Fpasswd could influence path interpretation in some HTTP clients. Suggest adding a strip of / and \ from the decoded result:
return filename ? decodeURIComponent(filename).replace(/[\/\\]/g, "-") : "asset";In-memory search DoS surface for authenticated users
The 1,000-row fallback cap is a significant improvement, but a user with write-heavy access could still trigger repeated 1,000-row scans. If the adapter ever gains native substring search, the in-memory path should be removed rather than just capped.
Error messages propagated to the UI
The errorMessage callbacks in useRegisterAssetForm and useCreateFolderForm surface error.message directly:
errorMessage: (error) => error.message || t("media.toasts.registerError", "...")This is consistent with the prior implementation but means detailed backend error strings (including any internal context) reach the browser. Ensure the backend throws only user-safe messages on validation/permission failures.
Checklist summary
| Category | Finding |
|---|---|
| SQL / command injection | None — adapter where API used exclusively; in-memory filter uses .includes() |
| Path traversal | Low risk in metadata only; filenameFromUrl decoding noted above |
| Authz bypass | None — client guards are presentation-only; backend hooks unchanged |
| Secrets / token leakage | None — no new logging of sensitive fields |
| SSRF | None — URL allowlist enforcement predates and is unmodified by this PR |
| XSS | None — asset URLs rendered via React JSX (text/attribute encoding); confirm() uses plain text |
| Unsafe deserialization | None |
| Dependency risk | None — no new external dependencies introduced |
Sent by Cursor Automation: Find vulnerabilities
|
✅ Shadcn registry validated — no registry changes detected. |



Summary
Compatibility notes
mediaAssets/mediaFoldersresource prefixesuseFolders(undefined)lists all folders;useFolders(null)lists root folders via the__root__wire sentinelTest plan
pnpm buildpnpm typecheckpnpm lintpnpm testpnpm knipNote
Medium Risk
Broad Media UI and cache-key changes affect listing, search, and picker freshness; permissions are presentation-only and backend hooks remain the security boundary. Folder list wire semantics (
__root__) and query prefix changes could break consumers relying on old keys or ambiguoususeFoldersbehavior.Overview
This PR finishes the Media plugin’s v3 client sweep: JSON asset/folder operations move onto the shared resource factory (
mediaResources/createResource), with newuseRegisterAssetFormanduseCreateFolderFormfor custom forms (field errors, notify, cache invalidation). Uploads stay on the custom transport; create/delete/register invalidations userefetchType: "all"so inactive Browse lists refresh after Upload/URL tabs.Query keys and API semantics are corrected: prefixes are
mediaAssets/mediaFolders;useFolders(undefined)vsuseFolders(null)are distinct (all folders vs root via__root__sentinel). Asset search is trimmed, capped at 200 chars, and server search scans at most the newest 1,000 rows before pagination.Standalone
/mediasyncsfolderand debouncedqto the URL viauseListState; the embedded MediaPicker keeps folder/search local. UI wiresuseCan/CanAccess,useTranslate, anduseNotify(replacing direct toasts), gates create/upload/URL/delete controls, and protects the library route withmedia:assetread. Docs, registry JSON, unit tests, client sweep tests, and E2E for URL folder/search behavior are updated.The client resource layer also gains optional
refetchTypeon mutations for the same inactive-cache behavior.Reviewed by Cursor Bugbot for commit b05a8c6. Bugbot is set up for automated code reviews on this repo. Configure here.