feat(ui-builder): complete v3 plugin sweep - #148
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Security Review — feat(ui-builder): complete v3 plugin sweep
Result: No high-confidence vulnerabilities found. The PR is primarily a refactor (query-key extraction, hook simplification, i18n/notify plumbing) plus a meaningful security improvement. Analysis by area follows.
✅ Security improvement: client-side permission gating added
Previously, the Edit and Delete actions in the page list had no CanAccess guard — any authenticated user could see and invoke them regardless of role. This PR wraps each action correctly:
// page-list-page.internal.tsx (new)
<CanAccess resource="ui-builder:page" action="update" params={{ id: page.id }}>
<DropdownMenuItem …>Edit</DropdownMenuItem>
</CanAccess>
<CanAccess resource="ui-builder:page" action="delete" params={{ id: page.id }}>
<DropdownMenuItem …>Delete</DropdownMenuItem>
</CanAccess>Similarly, PageListPage and PageBuilderPage now pass a permission prop to ComposedRoute, enforcing a read/create/update check at the route boundary. This is additive hardening.
⚠️ Low-confidence observations (not blocking, context-dependent)
1. Error messages surface to the admin UI
DefaultErrorComponent (and DefaultErrorComponent in page-renderer.tsx) renders raw error messages:
{(error instanceof Error ? error.message : undefined) || unexpectedError}This is a React text node — no XSS risk. However, if the backend ever returns error messages containing internal details (stack traces, DB schema hints), those would appear in the admin UI. Since this panel is admin-only and the pattern existed before the PR, this is low severity, but the server should ensure error responses are appropriately sanitized before reaching the client.
2. bySlug hook passes the slug as a raw query parameter without hook-level validation
// query-keys.ts
bySlug: {
query: (slug: string) => ({ slug, limit: 1 }),The public useUIBuilderPageBySlug(slug) hook accepts an arbitrary string. If a consumer passes a value sourced from URL params without sanitizing it first (e.g. useUIBuilderPageBySlug(params.slug)), the raw value is forwarded as a query parameter. The server must validate and reject unexpected values. The hook could defensively validate the slug, but this is a consumer-side concern.
3. External registry URL is pinned to a branch HEAD (pre-existing)
// build-registry.ts (pre-existing entry, not introduced by this PR)
"https://raw.githubusercontent.com/olliethedev/ui-builder/refs/heads/main/registry/block-registry.json"This URL resolves to the live main branch, not a tagged release or commit hash. A supply-chain compromise of that upstream repo would be pulled in on the next registry rebuild. This risk is not introduced by this PR — the URL was already present and the new EXTERNAL_ONLY_REGISTRY_COMPONENTS logic correctly routes to it rather than embedding local copies. It is flagged here for awareness. Pinning to a commit SHA or version tag would harden this.
No findings in
- Injection (SQL / command / path traversal):
typeSlugis hardcoded toUI_BUILDER_TYPE_SLUG; no user-controlled values flow into URL path segments.slugin create body is constrained client-side by^[a-z0-9-]+$before submission. - Secrets / token leakage: No credentials logged or embedded.
headersforwarded from plugin overrides follows the established pattern. - SSRF / open redirect: Post-create redirect uses
basePath(host-controlled) + server-returnedpage.id, not user input. - Unsafe deserialization:
layersandvariablesareJSON.stringify-ed client-side before transmission; the server is responsible for schema validation on ingest. - Dependency / supply-chain (new): No new third-party packages added.
sonneris removed as a dependency from this plugin (replaced byuseNotify), which is a positive reduction in surface area. - Auth bypass: All client-side permission gates (
CanAccess,ComposedRoute permission) are UI convenience — the authoritative enforcement remains server-side as expected.
Sent by Cursor Automation: Find vulnerabilities
|
✅ Shadcn registry validated — no registry changes detected. |


Summary
Verification
pnpm buildpnpm typecheckpnpm knippnpm --filter @btst/stack test— 476 tests passedpnpm --filter @btst/stack test-registrycd docs && pnpm buildv3:packages/ui/src/components/ui-builder/**packages/ui/src/lib/ui-builder/**Notes
The pre-existing workspace change to
pnpm-lock.yamlis intentionally excluded.Note
Cursor Bugbot is generating a summary for commit 7c663c0. Configure here.