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.

Why is this an issue?

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.

Understanding expression and variable expansion

Azure Pipelines expressions and variables are expanded before the shell script runs:

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.

What is the potential impact?

The consequences of successful script injection in Azure Pipelines can be severe:

How to fix it

Code examples

Noncompliant code example

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.

Compliant solution

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'

How does this work?

The key difference lies in when and how the data is processed:

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.

Resources

Documentation

Standards