Azure Pipelines task inputs should not directly interpolate untrusted parameters or variables to avoid injection attacks when values are controlled at queue time or come from other untrusted sources.
Passing untrusted data directly to a pipeline task creates two distinct injection risks.
For shell script tasks like Bash@3 or PowerShell@2, a parameter interpolated directly into the inline script
input is expanded before the shell parses it. If the value contains shell metacharacters such as ; rm -rf / or $(whoami),
the shell treats them as executable code, enabling arbitrary command execution.
Non-shell tasks are also at risk. A task that passes a parameter as a command-line argument can be affected by argument injection even without a shell. An attacker does not need shell metacharacters; supplying an unexpected value such as an extra flag or a different target name is enough to alter the task’s behavior.
Azure Pipelines can use parameters and variables that may be set or overridden by users when a pipeline is run (for example, at queue time). If
this untrusted data is interpolated directly into task inputs using Azure’s expression syntax (${{ … }}) or variable syntax
($(VARIABLE_NAME)), an attacker who can queue a pipeline run can exploit it.
The consequences of successful parameter injection attacks in Azure Pipelines can be severe:
The fix depends on the type of task.
For shell script tasks like Bash@3, assign untrusted data to environment variables using the env key, then reference them
with standard shell variable syntax ($VARIABLE_NAME or ${VARIABLE_NAME}) instead of Azure’s expression syntax. The shell
receives the value as data rather than code, preventing injection. Alternatively, use the values keyword to restrict the parameter to a
predefined safe list — the pipeline engine enforces this before any task runs.
For non-shell tasks, the env approach does not apply. Instead, use the values keyword to restrict the parameter to a safe
predefined list. If values cannot be used, validate the input in a preceding script task (using the env pattern) before
passing it to the task.
The following pipeline is vulnerable to command injection because the Bash@3 task receives untrusted data directly in its inline
script input:
parameters:
- name: DeployMessage
type: string
default: 'Deployment started'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: Bash@3
inputs:
targetType: inline
script: |
echo "Status: ${{ parameters.DeployMessage }}" # Noncompliant
./deploy.sh
If a user queues the pipeline with DeployMessage set to "; rm -rf / #", the shell executes it as multiple commands
instead of treating it as a simple message string.
Pass untrusted data via the step’s env block, then reference it with normal shell variable syntax in the script. The pipeline expands
the expression when setting the environment variable; the shell then treats the value as data, not as part of the script:
parameters:
- name: DeployMessage
type: string
default: 'Deployment started'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: Bash@3
inputs:
targetType: inline
script: |
echo "Status: $DEPLOY_MESSAGE"
./deploy.sh
env:
DEPLOY_MESSAGE: ${{ parameters.DeployMessage }}
The following pipeline passes an untrusted parameter directly to a non-shell task, enabling argument injection:
parameters:
- name: BuildConfig
type: string
default: 'Debug'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: DotNetCoreCLI@2
inputs:
command: build
arguments: '--configuration ${{ parameters.BuildConfig }}' # Noncompliant
An attacker can queue the pipeline with BuildConfig set to Debug --output /attacker/path, injecting an extra flag into
the dotnet command arguments.
Restrict the parameter to a predefined list of safe values using values. The pipeline engine rejects any queue-time value not in the
list before it reaches the task:
parameters:
- name: BuildConfig
type: string
default: 'Debug'
values:
- Debug
- Release
pool:
vmImage: 'ubuntu-latest'
steps:
- task: DotNetCoreCLI@2
inputs:
command: build
arguments: '--configuration ${{ parameters.BuildConfig }}'
The key difference lies in when and how the untrusted data is processed:
${{ parameters.DeployMessage }} directly in a task input or inline
script, Azure Pipelines first substitutes the expression with the actual parameter value, then passes the resulting string to the task. If the value
contains shell metacharacters, they become part of the command syntax and get executed.values list, the pipeline engine validates
the input at queue time before any task sees it. Only the pipeline author controls the allowed values, so an attacker cannot supply an unexpected
value regardless of how the task uses the parameter.This separation ensures that malicious syntax or unexpected values in the untrusted data cannot affect the task’s execution.