Skip to content

fix(slack): scope incident resolve/decide clicks to the owning workspace#162

Open
arseniycodes wants to merge 1 commit into
mainfrom
ash/sec-slack-incident-tenant-guard
Open

fix(slack): scope incident resolve/decide clicks to the owning workspace#162
arseniycodes wants to merge 1 commit into
mainfrom
ash/sec-slack-incident-tenant-guard

Conversation

@arseniycodes

@arseniycodes arseniycodes commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Problem

handleSlackResolveIncident and handleProposalDecision look up the incident/proposal by bare id and resolve/close it before consulting any installation, and the DB helpers (resolveIncident, confirm/dismissResolutionProposal) filter on id only — no project/tenant predicate. So the only thing binding a resolve click to a tenant is the Slack signing-secret check on the request.

Impact

Latent cross-workspace write: an interactivity payload carrying another tenant's incident/proposal id would resolve or silence their incident. It is not forgeable by an ordinary user — Slack signs only the action_ids our app set on that workspace's own messages — so this is defence-in-depth against a signing-secret compromise or a future refactor that lets an id flow from user-editable input. The dashboard-side equivalents already scope correctly; this closes the inconsistency.

Fix

Add clickWorkspaceOwnsPin(pinnedInstallationId, payloadTeamId): before mutating, assert the clicking workspace owns the target's pinned Slack installation (its team_id matches payload.team.id). Cross-workspace clicks are logged and ignored. Unpinned/legacy rows return true and fall through to the existing team-scoped fallback, so legitimate same-workspace clicks are unaffected.


Summary by cubic

Scopes Slack incident resolve/decision clicks to the owning workspace to prevent cross-workspace mutations. Cross-tenant clicks are logged and ignored; same-workspace and legacy/unpinned rows continue to work.

  • Bug Fixes
    • Added clickWorkspaceOwnsPin(...) to verify the clicking workspace (payload.team.id) matches the target’s pinned installation (slack_installations.team_id) before any mutation.
    • Guarded handleSlackResolveIncident and handleProposalDecision with this check to close a latent cross-workspace write gap.

Written for commit 894337a. Summary will update on new commits.

Review in cubic

handleSlackResolveIncident and handleProposalDecision looked up the
incident/proposal by bare id and resolved/closed it before consulting
any installation, and the underlying helpers (resolveIncident,
confirm/dismissResolutionProposal) filter by id only — no project/tenant
predicate. The interactivity payload is signed with the app signing
secret and Slack only signs action_ids from a workspace's own messages,
so this isn't forgeable by an ordinary user today — but it's a latent
cross-workspace write with zero tenant enforcement at the mutation.

Add clickWorkspaceOwnsPin(): before mutating, assert the workspace that
sent the click (payload.team.id) owns the target's pinned Slack
installation. Cross-workspace clicks are ignored. Unpinned/legacy rows
fall through to the existing team-scoped fallback unchanged.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant