fix(coderd): prevent cross-tenant workspace app rebinding (#26103)

This commit is contained in:
dylanhuff-at-coder
2026-06-11 15:02:41 -04:00
committed by GitHub
parent 112c921235
commit f3c7c23623
7 changed files with 751 additions and 2 deletions
@@ -1704,6 +1704,7 @@ func (s *server) completeTemplateImportJob(ctx context.Context, job database.Pro
slog.F("transition", transition))
if err := InsertWorkspaceResource(ctx, db, jobID, transition, resource, telemetrySnapshot); err != nil {
s.warnWorkspaceAppRebindRejected(ctx, jobID, err)
return xerrors.Errorf("insert resource: %w", err)
}
}
@@ -2122,6 +2123,7 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
InsertWorkspaceResourceWithAgentIDsFromProto(),
)
if err != nil {
s.warnWorkspaceAppRebindRejected(ctx, jobID, err)
return xerrors.Errorf("insert provisioner job: %w", err)
}
}
@@ -2590,6 +2592,7 @@ func (s *server) completeTemplateDryRunJob(ctx context.Context, job database.Pro
err := InsertWorkspaceResource(ctx, db, jobID, database.WorkspaceTransitionStart, resource, telemetrySnapshot)
if err != nil {
s.warnWorkspaceAppRebindRejected(ctx, jobID, err)
return xerrors.Errorf("insert resource: %w", err)
}
}
@@ -3614,6 +3617,32 @@ func insertAgentScriptsAndLogSources(ctx context.Context, db database.Store, age
return nil
}
type workspaceAppRebindError struct {
slug string
appID uuid.UUID
agentID uuid.UUID
}
func (e *workspaceAppRebindError) Error() string {
return fmt.Sprintf("workspace app slug %q with ID %q is already bound to a workspace-owned agent and cannot be rebound to an agent in another workspace or to an agent without a workspace; refusing to rebind to agent ID %q", e.slug, e.appID, e.agentID)
}
func (s *server) warnWorkspaceAppRebindRejected(ctx context.Context, jobID uuid.UUID, err error) {
slog.Helper()
var rebindErr *workspaceAppRebindError
if !errors.As(err, &rebindErr) {
return
}
s.Logger.Warn(ctx, "workspace app rebind rejected by SQL guard",
slog.F("job_id", jobID.String()),
slog.F("app_id", rebindErr.appID.String()),
slog.F("agent_id", rebindErr.agentID.String()),
slog.F("app_slug", rebindErr.slug),
)
}
func insertAgentApp(ctx context.Context, db database.Store, agentID uuid.UUID, app *sdkproto.App, appSlugs map[string]struct{}, snapshot *telemetry.Snapshot) error {
// Similar logic is duplicated in terraform/resources.go.
slug := app.Slug
@@ -3702,6 +3731,17 @@ func insertAgentApp(ctx context.Context, db database.Store, agentID uuid.UUID, a
Tooltip: app.Tooltip,
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// The upsert's ON CONFLICT guard refused to rebind an app
// owned by a workspace to an agent outside that workspace,
// including agents from import or dry-run jobs that resolve
// to no workspace (SEC-91).
return &workspaceAppRebindError{
slug: slug,
appID: id,
agentID: agentID,
}
}
return xerrors.Errorf("upsert app: %w", err)
}