azure-devops-sc-exposes-client-secret
An ARM service connection (secret-based) injects the backing SP's client secret / certificate into the job environment; pipeline code can read and exfiltrate it.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
where
node_type(?pipeline) ==
BuildWorker
node_type(?sc_identity) == MachineIdentity
?sc_identity is backed by a secret-based ARM service connection (not WIF)
emit
| source type | BuildWorker |
|---|---|
| target type | Credential |
| source | ?pipeline |
| target | client secret / certificate for {sc_identity.name} |
| state logic | ACTIVE for every secret-based ARM service connection used by the pipeline; the secret is injected by the agent before each task that references the SC. The credential is LONG-LIVED (client secret) — not a short-lived token. Inherits CONDITIONAL(service_state) if the SC itself is CONDITIONAL. |
Narrative
{pipeline.name} runs as service connection {sc_identity.name} using a stored client secret; that secret is injected into the job environment and is readable by any code running in the pipeline, including attacker-injected steps. The credential is long-lived and usable outside the pipeline context.
Raw rule rules/derived/azure/devops.yaml
id: azure-devops-sc-exposes-client-secret
emits: ExposesCredential
description: An ARM service connection (secret-based) injects the backing SP's client secret / certificate
into the job environment; pipeline code can read and exfiltrate it.
applies_to:
- azure
match:
- - pipeline: null
- ExecutesAs
- sc_identity: null
where:
- node_type(?pipeline) == BuildWorker
- node_type(?sc_identity) == MachineIdentity
- ?sc_identity is backed by a secret-based ARM service connection (not WIF)
emit:
source_type: BuildWorker
target_type: Credential
source: ?pipeline
target: client secret / certificate for {sc_identity.name}
permissions: []
conditions: []
state_logic: "ACTIVE for every secret-based ARM service connection used by the pipeline; the secret\
\ is injected by the agent before each task that references the SC. The credential is LONG-LIVED (client\
\ secret) \u2014 not a short-lived token. Inherits CONDITIONAL(service_state) if the SC itself is\
\ CONDITIONAL."
confidence: 0.9
derived_from:
- azure-devops-pipeline-executes-as-sc
false_positive_note: "Do NOT emit ExposesCredential for WIF (federated credential) service connections\
\ \u2014 there is no stored secret to expose. Only emit for service connections where clientSecret\
\ or certificate is present in the SC configuration (GET serviceendpoint/endpoints/{id} shows type\
\ and auth scheme). Secret rotation or expiry: if the SP's secret is expired, downgrade to CONDITIONAL(service_state).\
\ The exposed secret is reusable outside the pipeline (long-lived) \u2014 this is a significant difference\
\ from a short-lived OIDC token."
narrative: '{pipeline.name} runs as service connection {sc_identity.name} using a stored client secret;
that secret is injected into the job environment and is readable by any code running in the pipeline,
including attacker-injected steps. The credential is long-lived and usable outside the pipeline context.'