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.

Why is this an issue?

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.

What is the potential impact?

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

How to fix it

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.

Code examples

Noncompliant code example

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.

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: 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 }}

Noncompliant code example

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.

Compliant solution

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 }}'

How does this work?

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

This separation ensures that malicious syntax or unexpected values in the untrusted data cannot affect the task’s execution.

Resources

Documentation

Standards

Related rules