Azure Pipelines scripts should not interpolate parameters or variables directly into script blocks, to avoid script injection when values are controlled at queue time or from other untrusted sources.
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). When
this data is interpolated directly into script blocks using Azure’s expression syntax (${{ … }}) or variable syntax
($(VARIABLE_NAME)), it can lead to script injection vulnerabilities. A user who can set parameter values or variables when running the
pipeline could supply values containing shell metacharacters and potentially execute arbitrary commands on the agent.
Azure Pipelines expressions and variables are expanded before the shell script runs:
${{ … }}): Used for parameters (e.g. ${{ parameters.EXAMPLE_PARAMETER }})
and variables (e.g. ${{ variables.MY_VARIABLE }}). These are evaluated when the pipeline is compiled. The resulting string is then
inserted into the script and passed to the shell.$(VARIABLE_NAME)): Variable references in the script are also expanded by the pipeline runtime
before the script is executed. The value (which may contain shell metacharacters) is substituted into the script, and the shell then receives the
already-expanded script.In both cases, if the value comes from a string parameter or from a variable that is derived from such input, an attacker who controls that value
can inject shell commands. For example, if a parameter is set to "; rm -rf / #", the script may be transformed into multiple shell
commands instead of a single safe string.
Parameters of type string (with no values restriction) are untrusted when provided at queue time. Variables that are
defined from such parameters (e.g. variables: MY_VAR: ${{ parameters.MY_PARAM }}) are also untrusted when used in script blocks. Using
them directly in script, bash, pwsh, or similar steps is therefore unsafe.
The consequences of successful script injection in Azure Pipelines can be severe:
The following pipeline is vulnerable to script injection because it uses a string parameter and variables directly in script steps. Values are expanded before the shell runs, so an attacker who can set the parameter or variable at queue time could inject shell metacharacters:
parameters:
- name: PackageName
type: string
default: 'nginx'
variables:
PACKAGE_NAME: ${{ parameters.PackageName }}
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
echo "Installing ${{ parameters.PackageName }}" # Noncompliant: string parameter expanded in script
- script: |
echo "${{ variables.PACKAGE_NAME }}" # Noncompliant: variable (from parameter) expanded in script
- script: |
echo "$(PACKAGE_NAME)" # Noncompliant: variable expanded in script
If a user queues the pipeline with PackageName set to "; rm -rf / #", the first script becomes echo "Installing ";
rm -rf / #", which the shell executes as multiple commands.
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: PackageName
type: string
default: 'nginx'
variables:
PACKAGE_NAME: ${{ parameters.PackageName }}
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
echo "Installing $PACKAGE_NAME_ENVVAR"
env:
PACKAGE_NAME_ENVVAR: ${{ parameters.PackageName }}
- script: |
echo "$PACKAGE_NAME_ENVVAR"
env:
PACKAGE_NAME_ENVVAR: ${{ variables.PACKAGE_NAME }}
- script: |
echo "$PACKAGE_NAME_ENVVAR"
env:
PACKAGE_NAME_ENVVAR: $(PACKAGE_NAME)
When the parameter has a values list, it is safe to use directly in the script because only the pipeline author controls the allowed
values:
parameters:
- name: PackageName
type: string
default: 'nginx'
values:
- nginx
- apache2
- postgresql
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
echo "Installing ${{ parameters.PackageName }}" # Compliant
displayName: 'Install package'
The key difference lies in when and how the data is processed:
${{ parameters.PackageName }} or $(PACKAGE) directly in
a script step, Azure Pipelines expands the expression or variable before the shell runs:
env block, the pipeline still
expands the expression when setting the environment variable, but the script uses normal shell variable syntax:
${{ parameters.PackageName }} and assigns the value to an environment variable (e.g.
PACKAGE_NAME)$PACKAGE_NAME)When the shell evaluates $PACKAGE_NAME, it performs variable expansion: the value is used as a string. Even if the value contains
shell metacharacters, they remain literal characters and are not interpreted as commands. That separation prevents malicious content from being
executed.
values list (safe when used directly): When a parameter specifies a values list,
only those values can be selected at queue time. Using the parameter directly in a script is safe because:
values list