Summary
api.v1.auth.user-actor-token.ts describes UATs as a strict downgrade of a PAT and accepts an optional cap. However, the legacy authenticateApiRequestWithPersonalAccessToken() helper accepts UATs and returns only { userId }, dropping the verified ability/cap. POST /api/v1/projects/:projectRef/alertChannels uses that helper and performs no UAT cap or ability check before calling CreateAlertChannelService.
A token capped to ["read:runs"] can therefore create a production webhook alert channel with attacker-controlled URL, secret, alert types, and environment types.
Impact
A holder of a delegated read-only UAT for a user in the target organization can modify project alert routing. With a webhook channel, this can route future deployment and error-group alerts to attacker-controlled infrastructure, creating project configuration integrity impact and potential confidentiality impact for future alert metadata.
Reproduction
I used a local, non-destructive harness that executes the real current source bodies for:
authenticateApiRequestWithPersonalAccessToken()
api.v1.projects.$projectRef.alertChannels.action()
CreateAlertChannelService.call()
CreateAlertChannelService.#createProperties()
Command:
timeout 120 node candidates/triggerdotdev_trigger_dev_uat-cap-alert-channel-write-bypass_2026-06-30/proof/trigger_uat_cap_alert_channel_harness.js
Observed:
- UAT claims contain
cap: ["read:runs"].
- The simulated cap ability denies alert-channel writes.
- The legacy helper returns only
{ userId: "attacker-user" }.
- The route returns 200.
- The service reaches
projectAlertChannel.create() for a production webhook alert channel.
Summary
api.v1.auth.user-actor-token.tsdescribes UATs as a strict downgrade of a PAT and accepts an optionalcap. However, the legacyauthenticateApiRequestWithPersonalAccessToken()helper accepts UATs and returns only{ userId }, dropping the verified ability/cap.POST /api/v1/projects/:projectRef/alertChannelsuses that helper and performs no UAT cap or ability check before callingCreateAlertChannelService.A token capped to
["read:runs"]can therefore create a production webhook alert channel with attacker-controlled URL, secret, alert types, and environment types.Impact
A holder of a delegated read-only UAT for a user in the target organization can modify project alert routing. With a webhook channel, this can route future deployment and error-group alerts to attacker-controlled infrastructure, creating project configuration integrity impact and potential confidentiality impact for future alert metadata.
Reproduction
I used a local, non-destructive harness that executes the real current source bodies for:
authenticateApiRequestWithPersonalAccessToken()api.v1.projects.$projectRef.alertChannels.action()CreateAlertChannelService.call()CreateAlertChannelService.#createProperties()Command:
Observed:
cap: ["read:runs"].{ userId: "attacker-user" }.projectAlertChannel.create()for a production webhook alert channel.