Repository navigation
Description
Security Finding
Severity: medium
Type: unsafe-pattern (trust-tier policy bypass / provenance laundering)
The workflow marketplace (#17714) polices the envelope tier of a published bundle but not the model's own provenance, and importers prefer the model's provenance over the envelope tier. Together this lets any registered spoke ship a model that other hives store and display as curated ("Reviewed and vouched for by a human") without a hub admin ever vouching for it.
Hub side — src/pkg/hub/marketplace.go handleMarketplacePublish: a non-admin publisher (any spoke with a valid heartbeat bearer) may assert local, local-published or ingested-unreviewed in bundle.Tier; curated/built-in are rejected with 403. But bundle.Model.Spec.Meta.Provenance is never checked. It is covered by the bundle hash (bundle.CanonicalHash), so the hub cannot normalise it either; it stores the bundle verbatim with Tier=local-published and Model.Spec.Meta.Provenance=curated.
Spoke side — src/pkg/dashboard/api_workflows_marketplace.go importBundle:
if m.Spec.Meta.Provenance == "" {
m.Spec.Meta.Provenance = workflowmodel.Provenance(b.Tier)
}
if m.Spec.Meta.Provenance == workflowmodel.ProvenanceBuiltIn || string(m.Spec.Meta.Provenance) == bundle.TierLocalPublished {
m.Spec.Meta.Provenance = workflowmodel.ProvenanceLocal
}Only an empty provenance is replaced by the hub-policed tier, and only built-in/local-published are downgraded. A model carrying curated sails through st.Save as curated. The same applies to POST /api/workflows/import (direct bundle upload) where the Tier field is attacker-chosen anyway.
bundle.Verify does not cross-check Tier against Model.Spec.Meta.Provenance, so neither side catches the disagreement. Existing tests (TestMarketplaceImportFromHub, hub marketplace_test.go) only exercise bundles where tier and provenance agree.
Impact
- A registered spoke (heartbeat credential, no admin) publishes
foo@1withTier=local-publishedandModel.Spec.Meta.Provenance=curated. The hub accepts (201). The marketplace index correctly showslocal-published, but every hive that imports it gets a library entry badged curated — "Reviewed and vouched for by a human." (config-workflows-library.js), defeating the admin-only policy the hub documents for that tier. - Workflow models drive stage-tracker columns, label GC on rebind (✨ Workflow models: safe label garbage collection on rebind and model-defined human-gate replies #17710) and human-gate replies, so the trust badge is what an owner reads before binding a repo to the model. The flaw does not grant code execution; it launders a trust label.
Recommendation
- Spoke
importBundle: derive provenance from the envelope tier whenever the model claims a tier higher than the bundle asserts — i.e. ifProvenanceiscuratedorbuilt-inandb.Tieris not that same value, set it fromb.Tier(then apply the existingbuilt-in/local-published → localdowngrade). Keep today's behaviour when the two agree so admin-published curated bundles still import as curated. - Hub
handleMarketplacePublish: whenwho.adminis false, reject (403) a bundle whoseModel.Spec.Meta.Provenanceiscuratedorbuilt-in, mirroring the existingTiercheck — the hub cannot rewrite the hash-covered model, so refusing is the only honest option. - Tests: hub publish of a non-admin bundle with model provenance
curated→ 403; spoke import of aTier=local-publishedbundle with model provenancecurated→ stored aslocal; admin-published curated bundle still imports ascurated.
A PR implementing 1–3 follows.
Filed by sec-check agent (ACMM L6 — full mode)
🐝 Hive Agent: security | Instance: hosted-available-oke-11-placeholder-r05x | SHA: c95518e54
— hive: agent=sec-check backend=copilot model=claude-fable-5.1 copilot=1.0.88
Activity