gcp-tasks-execute-as-via-queue-sa-swap

Re-pointing the queue's OIDC/OAuth token SA (cloudtasks.queues.update + iam.serviceAccounts.actAs) causes every future dispatch to carry a token for the chosen SA - an attacker controlling the consumer endpoint captures the token and exercises the SA's privileges.

derived gcp emits CanExecuteAs

match

A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.

?principal CanModifyConfiguration ?queue ?principal CanPassIdentity ?sa

where

node_type(?queue) == Queue AND provider_type(?queue) == google.cloud.tasks.v2.Queue ?principal has confirmed iam.serviceAccounts.actAs on ?sa (enforced by CanPassIdentity) ?queue has httpTarget configured (HTTP target queue, not App Engine only) ?sa.privilege_level >= ?principal.privilege_level

optional

Present → upgrade the emitted state; absent → downgrade (not a hard requirement).

?principal CanModifyConfiguration ?queue ?principal ExposesCredential ?sa

emit

source typeIdentity
target typeIdentity
source<principal>
target<chosen service account>
permissionscloudtasks.queues.update iam.serviceAccounts.actAs
conditionsrole_compatibility trigger_exists network_reachability
state logicCONDITIONAL(trigger_exists) by default when the SA swap is confirmed but no confirmed endpoint control is established — the queue will eventually dispatch and expose the token once a task is created or dispatched, but the attacker does not yet capture it without controlling the consumer endpoint. ACTIVE when: (a) the principal also has gcp-tasks-uri-override-token-capture (matched optional[0]+[1]) confirming the attacker has set uriOverride to an attacker-controlled endpoint, OR (b) the principal separately controls the consumer endpoint (e.g., Cloud Run service with attacker-writable service account, or explicit network control). POTENTIAL(role_compatibility) when actAs on the SA is unknown/unresolved. Never stronger than the contributing CanModifyConfiguration / CanPassIdentity.

Narrative

{principal.name} can re-point {queue.name} to use {sa.name} as its OIDC/OAuth token SA (cloudtasks.queues.update + iam.serviceAccounts.actAs). Every future dispatch carries a token for {sa.name}. If the attacker controls the consumer endpoint (e.g., via uriOverride redirect), the token is captured and {sa.name}'s privileges are exercised; otherwise, the attacker influences the legitimate consumer's execution via the SA's privileges.

Raw rule rules/derived/gcp/tasks.yaml

id: gcp-tasks-execute-as-via-queue-sa-swap
emits: CanExecuteAs
description: "Re-pointing the queue's OIDC/OAuth token SA (cloudtasks.queues.update + iam.serviceAccounts.actAs)\
  \ causes every future dispatch to carry a token for the chosen SA \u2014 an attacker controlling the\
  \ consumer endpoint captures the token and exercises the SA's privileges."
match:
- - ?principal
  - CanModifyConfiguration
  - ?queue
- - ?principal
  - CanPassIdentity
  - ?sa
where:
- node_type(?queue) == Queue AND provider_type(?queue) == google.cloud.tasks.v2.Queue
- ?principal has confirmed iam.serviceAccounts.actAs on ?sa (enforced by CanPassIdentity)
- ?queue has httpTarget configured (HTTP target queue, not App Engine only)
- ?sa.privilege_level >= ?principal.privilege_level
optional:
- - ?principal
  - CanModifyConfiguration
  - ?queue
- - ?principal
  - ExposesCredential
  - ?sa
emit:
  source_type: Identity
  target_type: Identity
  source: <principal>
  target: <chosen service account>
  permissions:
  - cloudtasks.queues.update
  - iam.serviceAccounts.actAs
  conditions:
  - role_compatibility
  - trigger_exists
  - network_reachability
  state_logic: "CONDITIONAL(trigger_exists) by default when the SA swap is confirmed but no confirmed\
    \ endpoint control is established \u2014 the queue will eventually dispatch and expose the token once\
    \ a task is created or dispatched, but the attacker does not yet capture it without controlling the\
    \ consumer endpoint. ACTIVE when: (a) the principal also has gcp-tasks-uri-override-token-capture\
    \ (matched optional[0]+[1]) confirming the attacker has set uriOverride to an attacker-controlled\
    \ endpoint, OR (b) the principal separately controls the consumer endpoint (e.g., Cloud Run service\
    \ with attacker-writable service account, or explicit network control). POTENTIAL(role_compatibility)\
    \ when actAs on the SA is unknown/unresolved. Never stronger than the contributing CanModifyConfiguration\
    \ / CanPassIdentity."
  confidence: min(contributing_confidences) * 0.85
  derived_from:
  - ?principal CanModifyConfiguration ?queue (gcp-tasks-update-queue-config)
  - ?principal CanPassIdentity ?sa (gcp-tasks-passidentity-queue-sa)
  - ?principal ExposesCredential ?sa via uriOverride? [optional, elevates to ACTIVE]
  false_positive_note: "The SA token is delivered to the consumer endpoint, not directly to the attacker.\
    \ CONDITIONAL(trigger_exists) is the safe default: without confirmed endpoint control, the attacker\
    \ influences the consumer's execution via the SA's token, but the legitimate consumer receives and\
    \ processes the token. Full CanExecuteAs(SA) requires the attacker to also control the consumer endpoint\
    \ (e.g., via gcp-tasks-uri-override-token-capture redirecting to an attacker endpoint) to capture\
    \ the raw token. Dedupes with can-execute-as execute-as-via-config-identity-swap on (CanExecuteAs,\
    \ ?principal, ?sa) \u2014 emit once. Only emit when ?sa.privilege_level >= ?principal.privilege_level.\
    \ Requires actAs \u2014 never emit without confirmed actAs."
  narrative: '{principal.name} can re-point {queue.name} to use {sa.name} as its OIDC/OAuth token SA (cloudtasks.queues.update
    + iam.serviceAccounts.actAs). Every future dispatch carries a token for {sa.name}. If the attacker
    controls the consumer endpoint (e.g., via uriOverride redirect), the token is captured and {sa.name}''s
    privileges are exercised; otherwise, the attacker influences the legitimate consumer''s execution
    via the SA''s privileges.'
move · open · esc close