gcp-cloudscheduler-uri-retarget-token-theft

Retargeting an existing HTTP job's URI (via jobs.update) to an attacker-controlled endpoint causes the existing SA's OIDC/OAuth2 bearer token to be delivered on the next invocation - no actAs on the SA required since the SA is unchanged.

derived gcp emits CanRetrieveToken

match

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

{'principal': None} CanModifyConfiguration {'job': None}

where

node_type(?job) == Scheduler ?job has an HTTP target with oidcToken.serviceAccountEmail or oauthToken.serviceAccountEmail set (a SA-authenticated HTTP job) ?principal does NOT need to hold iam.serviceAccounts.actAs on the existing SA — URI-only update does not require actAs

emit

source typeIdentity
target typeToken
source<principal>
target<OIDC/OAuth2 bearer token of the existing SA configured in the job>
permissionscloudscheduler.jobs.update
conditionsservice_state
state logicACTIVE when the principal holds cloudscheduler.jobs.update on the job AND the job is ENABLED (or PAUSED but can be run via jobs.run). The bearer token is delivered on the next scheduled invocation (or on manual jobs.run trigger). If the principal also holds cloudscheduler.jobs.run, immediate delivery is possible (ACTIVE). Without jobs.run: ACTIVE but bounded by the schedule cadence (next firing time).

Narrative

{principal.name} can update {job.name}'s HTTP URI to an attacker-controlled endpoint (cloudscheduler.jobs.update) without needing actAs on the existing SA. On the next invocation, the Cloud Scheduler service agent delivers the existing SA's OIDC/OAuth2 bearer token to the attacker's endpoint - yielding a short-lived token for that SA.

Raw rule rules/derived/gcp/cloudscheduler.yaml

id: gcp-cloudscheduler-uri-retarget-token-theft
emits: CanRetrieveToken
description: "Retargeting an existing HTTP job's URI (via jobs.update) to an attacker-controlled endpoint\
  \ causes the existing SA's OIDC/OAuth2 bearer token to be delivered on the next invocation \u2014 no\
  \ actAs on the SA required since the SA is unchanged."
match:
- - principal: null
  - CanModifyConfiguration
  - job: null
where:
- node_type(?job) == Scheduler
- ?job has an HTTP target with oidcToken.serviceAccountEmail or oauthToken.serviceAccountEmail set (a
  SA-authenticated HTTP job)
- "?principal does NOT need to hold iam.serviceAccounts.actAs on the existing SA \u2014 URI-only update\
  \ does not require actAs"
emit:
  source_type: Identity
  target_type: Token
  source: <principal>
  target: <OIDC/OAuth2 bearer token of the existing SA configured in the job>
  permissions:
  - cloudscheduler.jobs.update
  conditions:
  - service_state
  state_logic: 'ACTIVE when the principal holds cloudscheduler.jobs.update on the job AND the job is ENABLED
    (or PAUSED but can be run via jobs.run). The bearer token is delivered on the next scheduled invocation
    (or on manual jobs.run trigger). If the principal also holds cloudscheduler.jobs.run, immediate delivery
    is possible (ACTIVE). Without jobs.run: ACTIVE but bounded by the schedule cadence (next firing time).'
  confidence: min(contributing_confidences) * 0.9
  derived_from:
  - ?principal CanModifyConfiguration ?job (gcp-cloudscheduler-update-config)
  false_positive_note: "This path requires the job to already have an SA-authenticated HTTP target (oidcToken\
    \ or oauthToken set). A job with no authentication configured (no oidcToken/oauthToken) does not produce\
    \ a bearer token \u2014 do not emit. The token received at the attacker's endpoint is a short-lived\
    \ OIDC JWT (1-hour TTL) or OAuth2 access token scoped to https://www.googleapis.com/auth/cloud-platform;\
    \ it can be used to call GCP APIs as the SA. The attacker must intercept the HTTP request (the retargeted\
    \ endpoint must be under attacker control). This is a weaker path than CanExecuteAs (no ongoing code\
    \ execution), but each invocation delivers a fresh token."
  narrative: "{principal.name} can update {job.name}'s HTTP URI to an attacker-controlled endpoint (cloudscheduler.jobs.update)\
    \ without needing actAs on the existing SA. On the next invocation, the Cloud Scheduler service agent\
    \ delivers the existing SA's OIDC/OAuth2 bearer token to the attacker's endpoint \u2014 yielding a\
    \ short-lived token for that SA."
move · open · esc close