forked from molecule-ai/molecule-core
CRITICAL (#164): POST /bundles/import — anon callers could create arbitrary workspaces with user-supplied system prompts, plugins, and secrets envelopes. Fixed by gating behind AdminAuth (bundleAdmin group). HIGH (#165): GET /bundles/export/:id — anon UUID probe leaked full system prompts, agent_card, plugins, memory for any workspace. GET /events + GET /events/:workspaceId — anon read of the append-only event log leaked org topology, workspace names, card fragments. Both moved into the same bundleAdmin / eventsAdmin groups. MEDIUM (#166): PUT /canvas/viewport — anon callers could reset shared viewport state. Gated via a scoped viewportAdmin group; GET stays open so canvas bootstraps without a bearer. GET /admin/liveness — operational-intel leak (scheduler cadence reveals work pattern). Inline AdminAuth on the single handler. All 6 routes use the same lazy-bootstrap admin auth the rest of the platform uses: zero-token installs fail-open, once any token exists every request must present a valid bearer. Known follow-up: canvas uses session cookies not bearer tokens (same pattern as #138). In multi-tenant production these canvas features — Events tab, Export/Duplicate, viewport persist — will return 401 once a workspace is token-enrolled. Needs cookie-accepting AdminAuth as a follow-up (tracked as option B in #138 triage discussion); a new issue will be filed for that scope. The security gain from closing #164 CRITICAL outweighs the canvas UX regression for tonight. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
175bc2de50
commit
186206b33f
@ -76,7 +76,10 @@ func Setup(hub *ws.Hub, broadcaster *events.Broadcaster, prov *provisioner.Provi
|
||||
// to catch stuck-but-not-crashed goroutines (the failure mode that caused
|
||||
// the 12h scheduler outage of 2026-04-14, issue #85). Any subsystem whose
|
||||
// last tick is older than 2× its expected interval is stale.
|
||||
r.GET("/admin/liveness", func(c *gin.Context) {
|
||||
//
|
||||
// #166: gated behind AdminAuth. Internal health state is an ops-intel leak
|
||||
// in production (scheduler tick cadence reveals fleet size + work pattern).
|
||||
r.GET("/admin/liveness", middleware.AdminAuth(db.DB), func(c *gin.Context) {
|
||||
snap := supervised.Snapshot()
|
||||
out := make(map[string]interface{}, len(snap))
|
||||
now := time.Now()
|
||||
@ -196,10 +199,16 @@ func Setup(hub *ws.Hub, broadcaster *events.Broadcaster, prov *provisioner.Provi
|
||||
r.GET("/registry/:id/peers", dh.Peers)
|
||||
r.POST("/registry/check-access", dh.CheckAccess)
|
||||
|
||||
// Events (not workspace-scoped — exempt from per-workspace auth)
|
||||
// Events — #165: gated behind AdminAuth. The raw event log contains org
|
||||
// topology, workspace names, and agent-card fragments; an unauth read
|
||||
// leaks the entire fleet structure. GET /events/:workspaceId is still
|
||||
// a cross-workspace read so it uses AdminAuth, not WorkspaceAuth.
|
||||
eh := handlers.NewEventsHandler()
|
||||
r.GET("/events", eh.List)
|
||||
r.GET("/events/:workspaceId", eh.ListByWorkspace)
|
||||
{
|
||||
eventsAdmin := r.Group("", middleware.AdminAuth(db.DB))
|
||||
eventsAdmin.GET("/events", eh.List)
|
||||
eventsAdmin.GET("/events/:workspaceId", eh.ListByWorkspace)
|
||||
}
|
||||
|
||||
// Remaining auth-gated workspace sub-routes — appended to wsAuth group declared above.
|
||||
{
|
||||
@ -274,10 +283,16 @@ func Setup(hub *ws.Hub, broadcaster *events.Broadcaster, prov *provisioner.Provi
|
||||
th := handlers.NewTerminalHandler(dockerCli)
|
||||
wsAuth.GET("/terminal", th.HandleConnect)
|
||||
|
||||
// Canvas Viewport
|
||||
// Canvas Viewport — #166: PUT gated behind AdminAuth so an anon caller
|
||||
// can't reset the shared viewport state for all users. GET remains open
|
||||
// because the canvas bootstraps without a bearer and needs the initial
|
||||
// viewport for first paint.
|
||||
vh := handlers.NewViewportHandler()
|
||||
r.GET("/canvas/viewport", vh.Get)
|
||||
r.PUT("/canvas/viewport", vh.Save)
|
||||
{
|
||||
viewportAdmin := r.Group("", middleware.AdminAuth(db.DB))
|
||||
viewportAdmin.PUT("/canvas/viewport", vh.Save)
|
||||
}
|
||||
|
||||
// Templates
|
||||
tmplh := handlers.NewTemplatesHandler(configsDir, dockerCli)
|
||||
@ -317,10 +332,18 @@ func Setup(hub *ws.Hub, broadcaster *events.Broadcaster, prov *provisioner.Provi
|
||||
// unpack locally instead of going through Docker exec.
|
||||
wsAuth.GET("/plugins/:name/download", plgh.Download)
|
||||
|
||||
// Bundles
|
||||
// Bundles — #164 + #165: both gated behind AdminAuth.
|
||||
// POST /bundles/import — CRITICAL: anon creation of arbitrary workspaces
|
||||
// with user-supplied config (system prompts,
|
||||
// plugins, secrets envelope). #164.
|
||||
// GET /bundles/export/:id — HIGH: full system prompts + memory for any
|
||||
// workspace by UUID probe. #165.
|
||||
bh := handlers.NewBundleHandler(broadcaster, prov, platformURL, configsDir, dockerCli)
|
||||
r.GET("/bundles/export/:id", bh.Export)
|
||||
r.POST("/bundles/import", bh.Import)
|
||||
{
|
||||
bundleAdmin := r.Group("", middleware.AdminAuth(db.DB))
|
||||
bundleAdmin.GET("/bundles/export/:id", bh.Export)
|
||||
bundleAdmin.POST("/bundles/import", bh.Import)
|
||||
}
|
||||
|
||||
// Org Templates
|
||||
orgDir := findOrgDir(configsDir)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user