feat(security): verify widget manifest signatures before iframe load (#39)#123
Merged
Conversation
…#39) Host-side enforcement of packagehub Ed25519 publisher signatures for external (iframe) marketplace widgets — closes the latent #39 / Q-M-13 hole where a tampered manifest could point entryUrl at attacker infrastructure. Native in-bundle widgets are unaffected; fail-closed for legacy/unverified entries. - gate install, boot-hydration, and render on packagehub's /verify endpoint - bind the verified version to the installed manifest via widgetInstallTarget so a yanked-latest can't verify one version while registering another's entryUrl Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#39) The spec's embedded widget config used a dead `entrypoint` field with zero consumers; packagehub publishes manifests with z.object({}).loose() so it was never enforced. Replace it with the real runtime shape (entryType / entryUrl / componentPath / mountPoints / ...) that widget-sdk and office-ui already use, so the signed field name is unambiguous across repos. Adds a widget-pack example + positive/negative validator tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-proof (#39) Addresses an independent-review finding: the prior widgetInstallTarget unit test pinned the helper's contract but not the call site, so a future revert that verified the page-level (newest non-yanked) version again would pass silently. Extract the install decision into a pure, dependency-injected installWidgetFromPackage() and test it directly: verify is always called with the manifest's source version; an external widget that fails (or throws during) verification is never installed or registered; native widgets skip verification. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
Author
Independent-context security review — outcomeRan three fresh-context adversarial reviewers (gate-bypass, version-binding, spec) before requesting merge. Clean:
Addressed:
Accepted residual (not a security defect):
|
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.
What & why
Closes #39 / Q-M-13 — WidgetFrame companion: no manifest signature verification before loading a widget URL. office-ui installed a marketplace
type: 'widget'package by registering itsWidgetManifestverbatim and loadingmanifest.entryUrlinto an iframe with no signature check, so a tampered manifest could pointentryUrlat attacker infrastructure. This is the host-side companion to the merged #15 (postMessage hardening).Latent, not active. Every built-in widget today is
entryType: "native"(in-bundle, no iframe) and the marketplace seed has onlytype: 'personality'packages — there are zero installable external widgets yet. This gate matters the moment the marketplace serves a third-party external widget.No new crypto. packagehub already signs the whole version manifest (Ed25519) and exposes
GET /packages/:name/versions/:version/verify. The signature covers the nestedwidgetobject, so averified: truemeans theentryUrlis exactly what the publisher signed. This PR is host-side enforcement only.Changes
office-ui — fail-closed enforcement at every checkpoint
verifyWidgetVersion()calls packagehub's/verify; strictverified === truecoercion.widgetLoadDecision(entryType, verified)gate (native → allow; external → allow iff verified).useInstalledWidgetsStoretracks verification in a parallelverifiedByWorkspacemap +isVerified()(fail-closed default); persistversion: 1migration leaves pre-existing entries unverified.<WidgetFrame>).Security fix found while finishing the work — verify/install version binding
pkg.latestVersion— which can be a yanked version. That let a yanked-latest verify version X while registering version Y'sentryUrl. NewwidgetInstallTarget()returns the manifest and the version from the samepkg.latestVersion, so they can't diverge. Regression-tested.Spec reconciliation (
@urule/spec)entrypointfield with zero consumers (packagehub publishes withz.object({}).loose(), so it was never enforced). Replaced with the real runtime shape (entryType/entryUrl/componentPath/mountPoints/…) that widget-sdk and office-ui already use, so the signed field name is unambiguous across repos. JSON schema updated with conditionalentryTyperequirements; added awidget-packexample + positive/negative validator tests.Tests
widget-frame-security(18) +useInstalledWidgetsStore(12) +marketplace-api(18, incl. the version-binding regression) — 48 pass.@urule/office-uiand@urule/spec.Out of scope / residuals (deferred)
/verifydoesn't echo the signedentryUrl— acceptable because the host verifies the same version it installs and both read the same packagehub DB row (publisher trust = package ownership; a packagehub compromise is out of model)..loose()to avoid touching non-widget package types).🤖 Generated with Claude Code