Namespace plugin-emitted hook events to plugin.<id>.*#6
Merged
Conversation
Any plugin granted cms.hooks could forge core/system events: handleHooksEmit passed the plugin-supplied event name straight to hookBus.emit, so plugin A could fire a fake content.entry.created at plugin B's listeners or spoof settings.changed / publish.* events. Plugin emits are now force-namespaced by the host (the kernel of correctness is handleHooksEmit, keyed on the worker-verified msg.pluginId, never plugin-supplied data): - canonicalPluginEventName (src/core/plugins/hookBus.ts): bare names are prefixed to plugin.<id>.<name> (a raw emit of content.entry.created becomes the harmless plugin.<id>.content.entry.created), names already in the plugin's own namespace pass through unchanged, and names in another plugin's namespace are rejected as impersonation. - CORE_HOOK_EVENTS is the single source of truth for the reserved host-emitted names; hookBus.emit is typed CoreHookEvent | PluginScopedHookEvent so host emit sites cannot drift from the list. - cms.hooks.emit now resolves to the canonical namespaced name so plugin authors can log/share the exact name other plugins must subscribe to. Subscriptions (cms.hooks.on) stay unrestricted — cross-plugin eventing works by listening to the full namespaced name. - SDK types (ServerPluginHooksApi.emit) and the VM bootstrap document the contract; docs/features/plugin-system.md gains the namespace rule + the reserved event list and drops the phantom media.uploaded event. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cbb3ea5 to
d20c602
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Security fix: any plugin granted
cms.hookscould forge core/system events.handleHooksEmit(server/plugins/host/handlers/hooks.ts) passed the plugin-supplied event name straight tohookBus.emit, so plugin A could fire a fakecontent.entry.createdat plugin B's listeners or spoofsettings.changed/publish.before/publish.after.Plugin-emitted events are now force-namespaced to
plugin.<pluginId>.<name>by the host, making event provenance explicit and unforgeable. The contract (one coherent rule — auto-prefix, never spoof):sync.done) is delivered asplugin.<id>.sync.done— including reserved core names, so a raw emit ofcontent.entry.createdbecomes the harmlessplugin.<id>.content.entry.createdplugin.<other-id>.*) is rejected with a clear impersonation errorcms.hooks.on) stay unrestricted — cross-plugin eventing works by listening to the full namespaced namecms.hooks.emitnow resolves to the canonical namespaced nameChanges
server/plugins/host/handlers/hooks.ts— kernel of correctness:handleHooksEmitcanonicalizes via the worker-verifiedmsg.pluginId(never plugin-supplied data) and replies with the canonical namesrc/core/plugins/hookBus.ts—canonicalPluginEventName()+CORE_HOOK_EVENTSas the single source of truth for reserved host events (settings.changed,content.entry.created/updated/deleted,publish.before/after);HookBus.emitis typedCoreHookEvent | PluginScopedHookEvent, so a host emit site cannot introduce a core event without adding it to the list (compile-time anti-drift)src/core/plugin-sdk/types/hooks.ts—ServerPluginHooksApi.emitdocuments the namespacing and returnsPromise<string>(the canonical name); fixed the cross-plugin naming exampleserver/plugins/quickjs/bootstrap/src/buildApi.ts— VM-side contract comment;bun run bootstrap:syncran (generated artifacts byte-identical — comments are stripped by bundling;plugin-bootstrap-fresh.test.tsgreen)docs/features/plugin-system.md— namespace rule, reserved host-event list, cross-plugin listening; removed the phantommedia.uploadedevent (documented but never emitted)src/core/plugins/runtime.ts/events.tsare unrelated surfaces) — nothing to change thereexamples/plugins/template/does not usehooks.emit— unchangedTests
src/__tests__/plugins/pluginHookBus.test.ts— unit coverage of all four canonicalization rules + host core emits still reaching bare-name listenerssrc/__tests__/server/pluginServerRuntime.test.ts— new end-to-end test through real plugin install/activate: bare emit lands namespaced; forgedcontent.entry.creatednever reaches core-name listeners; self-prefixed emit not double-prefixed; other-namespace emit rejected with a clear error; emit resolves to the canonical namecmsPlugins.test.ts,pluginServerRuntime.test.ts,pluginVmPermissions.test.ts)Verification
bun test— 4915 pass, 0 failbun run build(tsc -b && vite build) — cleanbun run lint— clean🤖 Generated with Claude Code