Skip to content

AzureCLI V1/V2/V3: isolate AZURE_CONFIG_DIR per task invocation#22221

Open
wawanawna wants to merge 7 commits into
masterfrom
fix/azurecli-config-poisoning
Open

AzureCLI V1/V2/V3: isolate AZURE_CONFIG_DIR per task invocation#22221
wawanawna wants to merge 7 commits into
masterfrom
fix/azurecli-config-poisoning

Conversation

@wawanawna
Copy link
Copy Markdown
Contributor

@wawanawna wawanawna commented May 29, 2026

What this changes

AzureCLI@1/@2/@3 used to point AZURE_CONFIG_DIR at a fixed path under the agent temp directory (Agent.TempDirectory/.azclitask).

On a shared self-hosted agent, an earlier pipeline step could pre-seed .azclitask/config with an attacker-controlled [extension] block — index_url, use_dynamic_install, run_after_dynamic_install — so that the next AzureCLI step's first az call would fetch and execute an attacker-controlled wheel under the service-connection identity.
The end-of-task cleanup also operated on the same fixed path, so the attacker dir could be re-seeded across steps.

This PR moves all three task versions to a per-invocation, unpredictable config dir.

How

  • createPerInvocationAzureConfigDir(agentTempDir) — uses fs.mkdtempSync to create Agent.TempDirectory/.azclitask-XXXXXX with a cryptographically random suffix (libuv tempchars, filesystem-safe on every supported OS). Available since Node 5.10 — well below every Node version the task handlers ship with.
  • removePerInvocationAzureConfigDir(path) — never-throws cleanup (tl.rmRF + delete process.env['AZURE_CONFIG_DIR']) so cleanup errors can't mask the original task failure.

For V2 and V3, scriptType.cleanUp() in the finally block is now wrapped in its own try/catch so a cleanup error there can't prevent the config-dir cleanup from running.

Behavior change for users

Cross-step persistence of az extension add / az config set between AzureCLI steps in the same job is no longer guaranteed — each step now gets a fresh, isolated config dir. The pre-existing useGlobalConfig: true input remains the supported escape hatch for pipelines that rely on persistence;

Tests

Adds Tests/L0ConfigDirIsolation.ts to each task — 8 L0 cases covering:

  1. Directory is created on disk under the agent temp root
  2. Two invocations produce different paths (unpredictability)
  3. Two invocations are mutually isolated (no shared files)
  4. New dir does not contain a pre-seeded config even if attacker has pre-populated the legacy path
  5. Throws on empty/undefined agentTempDir
  6. Cleanup removes a populated dir and unsets AZURE_CONFIG_DIR
  7. Cleanup is a no-op for null/undefined/empty input (safe in finally)
  8. Cleanup never throws when the path doesn't exist

Full L0 suite passes per task: V1 21 passing, V2 47 passing, V3 59 passing.

The AzureCLI@1/@2/@3 tasks previously pointed AZURE_CONFIG_DIR at the predictable path Agent.TempDirectory/.azclitask. Because that path is fixed and lives on a shared self-hosted agent, an earlier pipeline step running on the same agent could pre-seed .azclitask/config with an [extension] block (index_url, use_dynamic_install, run_after_dynamic_install) so that the next AzureCLI step's first az invocation would fetch and execute an attacker-controlled wheel under the service-connection identity. Cleanup only happened on the same predictable path, so the attacker dir could be re-seeded across steps.

This change introduces an AzureCliConfigDir.ts helper, used by all three task versions:

* createPerInvocationAzureConfigDir(agentTempDir) uses fs.mkdtempSync to create a fresh Agent.TempDirectory/.azclitask-XXXXXX directory with a cryptographically random suffix. An attacker can no longer predict the path, so pre-seeded config files at the legacy location are ignored.
* removePerInvocationAzureConfigDir(path) performs a never-throwing cleanup (rmRF + unset env var) so the per-invocation directory is removed even when the script body throws and so cleanup errors do not mask the original failure.

For V2 and V3, scriptType.cleanUp() in the finally block is now wrapped in its own try/catch so a cleanup error there cannot prevent the new config-dir cleanup from running.

Behavior change for users: cross-step persistence of az extension add or az config set between AzureCLI steps in the same job is no longer guaranteed (each step now gets a fresh, isolated config dir). The pre-existing useGlobalConfig: true input remains the supported escape hatch for pipelines that rely on persistence.

Tests: adds Tests/L0ConfigDirIsolation.ts to each task with eight L0 cases covering directory creation, uniqueness across invocations, mutual isolation, attacker-pre-seeding resistance, input validation, cleanup of populated directories, null/empty no-op, and missing-path no-op. Full L0 suite passes per task (V1 21, V2 47, V3 59).
@wawanawna wawanawna marked this pull request as ready for review May 29, 2026 12:04
@wawanawna wawanawna requested review from a team and manolerazvan as code owners May 29, 2026 12:04
@wawanawna
Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

CI 'Check for downgrading tasks' requires task version > master:

- AzureCLIV1: 1.274.0 -> 1.275.0 (minor < sprint)

- AzureCLIV2: 2.275.4 -> 2.275.6 (== master patch)

- AzureCLIV3: 3.275.8 -> 3.275.10 (== master patch)

Node24 variants auto-bumped to <Default>+1 by BuildConfigGen.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@wawanawna
Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

@wawanawna
Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

@wawanawna
Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

@wawanawna
Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

createPerInvocationAzureConfigDir now sets process.env.AZURE_CONFIG_DIR
to the new directory (matching the unset already done by
removePerInvocationAzureConfigDir on cleanup). Callers no longer need to
remember to mirror the env assignment after the mkdtempSync.

Behavior is unchanged: identical env-var lifetime as before (set in
beforeExecution, unset in finally), but the pair is now encapsulated in
one module so it cannot drift. The helper stays per-task (one copy in
each of V1/V2/V3) to match the repo convention of self-contained tasks.

Added L0 test asserting the env var is set after the call. Cleanup in
the first describe's afterEach unsets the var so tests stay isolated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@wawanawna wawanawna force-pushed the fix/azurecli-config-poisoning branch from c6ac5c1 to 6626762 Compare May 29, 2026 14:40
@wawanawna
Copy link
Copy Markdown
Contributor Author

Reverted the common-module extraction (commits 6b18ba1, e4e9f35, c6ac5c1). Kept only the env-set/unset symmetry, applied inside the per-task helper. Branch now: dbd8161 (fix), 290f025 (version bumps), 6626762 (env symmetry).

/azp run

@wawanawna
Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens AzureCLI@1/@2/@3 by moving AZURE_CONFIG_DIR from a predictable shared temp path to a per-invocation temp directory, reducing cross-step config pre-seeding risk on shared agents.

Changes:

  • Adds duplicated Azure CLI config-dir helper modules and L0 tests for V1/V2/V3.
  • Updates task execution cleanup to remove the per-invocation config directory.
  • Bumps task versions and generated build mappings for default and Node24 variants.

Reviewed changes

Copilot reviewed 57 out of 57 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Tasks/AzureCLIV1/azureclitask.ts Uses isolated config dir and cleanup in V1.
Tasks/AzureCLIV1/src/AzureCliConfigDir.ts Adds V1 helper for creating/removing isolated AZURE_CONFIG_DIR.
Tasks/AzureCLIV1/Tests/L0.ts Wires config-dir isolation tests into V1 L0 suite.
Tasks/AzureCLIV1/Tests/L0ConfigDirIsolation.ts Adds V1 helper unit coverage.
Tasks/AzureCLIV1/task.json Bumps V1 task version.
Tasks/AzureCLIV1/task.loc.json Mirrors V1 version bump.
Tasks/AzureCLIV2/azureclitask.ts Uses isolated config dir and cleanup in V2.
Tasks/AzureCLIV2/src/AzureCliConfigDir.ts Adds V2 helper for creating/removing isolated AZURE_CONFIG_DIR.
Tasks/AzureCLIV2/Tests/L0.ts Wires config-dir isolation tests into V2 L0 suite.
Tasks/AzureCLIV2/Tests/L0ConfigDirIsolation.ts Adds V2 helper unit coverage.
Tasks/AzureCLIV2/task.json Bumps V2 task version.
Tasks/AzureCLIV2/task.loc.json Mirrors V2 version bump.
Tasks/AzureCLIV3/azureclitask.ts Uses isolated config dir and cleanup in V3.
Tasks/AzureCLIV3/src/AzureCliConfigDir.ts Adds V3 helper for creating/removing isolated AZURE_CONFIG_DIR.
Tasks/AzureCLIV3/Tests/L0.ts Wires config-dir isolation tests into V3 L0 suite.
Tasks/AzureCLIV3/Tests/L0ConfigDirIsolation.ts Adds V3 helper unit coverage.
Tasks/AzureCLIV3/task.json Bumps V3 task version.
Tasks/AzureCLIV3/task.loc.json Mirrors V3 version bump.
_generated/AzureCLIV1/azureclitask.ts Mirrors V1 runtime changes.
_generated/AzureCLIV1/src/AzureCliConfigDir.ts Mirrors V1 helper.
_generated/AzureCLIV1/Tests/L0.ts Mirrors V1 test wiring.
_generated/AzureCLIV1/Tests/L0ConfigDirIsolation.ts Mirrors V1 helper tests.
_generated/AzureCLIV1/task.json Updates generated V1 version mapping.
_generated/AzureCLIV1/task.loc.json Updates generated V1 localized metadata.
_generated/AzureCLIV1.versionmap.txt Updates generated V1 version map.
_generated/AzureCLIV1_Node24/azureclitask.ts Mirrors V1 Node24 runtime changes.
_generated/AzureCLIV1_Node24/src/AzureCliConfigDir.ts Mirrors V1 Node24 helper.
_generated/AzureCLIV1_Node24/Tests/L0.ts Mirrors V1 Node24 test wiring.
_generated/AzureCLIV1_Node24/Tests/L0ConfigDirIsolation.ts Mirrors V1 Node24 helper tests.
_generated/AzureCLIV1_Node24/task.json Updates V1 Node24 task version.
_generated/AzureCLIV1_Node24/task.loc.json Updates V1 Node24 localized metadata.
_generated/AzureCLIV2/azureclitask.ts Mirrors V2 runtime changes.
_generated/AzureCLIV2/src/AzureCliConfigDir.ts Mirrors V2 helper.
_generated/AzureCLIV2/Tests/L0.ts Mirrors V2 test wiring.
_generated/AzureCLIV2/Tests/L0ConfigDirIsolation.ts Mirrors V2 helper tests.
_generated/AzureCLIV2/task.json Updates generated V2 version mapping.
_generated/AzureCLIV2/task.loc.json Updates generated V2 localized metadata.
_generated/AzureCLIV2.versionmap.txt Updates generated V2 version map.
_generated/AzureCLIV2_Node24/azureclitask.ts Mirrors V2 Node24 runtime changes.
_generated/AzureCLIV2_Node24/src/AzureCliConfigDir.ts Mirrors V2 Node24 helper.
_generated/AzureCLIV2_Node24/Tests/L0.ts Mirrors V2 Node24 test wiring.
_generated/AzureCLIV2_Node24/Tests/L0ConfigDirIsolation.ts Mirrors V2 Node24 helper tests.
_generated/AzureCLIV2_Node24/task.json Updates V2 Node24 task version.
_generated/AzureCLIV2_Node24/task.loc.json Updates V2 Node24 localized metadata.
_generated/AzureCLIV3/azureclitask.ts Mirrors V3 runtime changes.
_generated/AzureCLIV3/src/AzureCliConfigDir.ts Mirrors V3 helper.
_generated/AzureCLIV3/Tests/L0.ts Mirrors V3 test wiring.
_generated/AzureCLIV3/Tests/L0ConfigDirIsolation.ts Mirrors V3 helper tests.
_generated/AzureCLIV3/task.json Updates generated V3 version mapping.
_generated/AzureCLIV3/task.loc.json Updates generated V3 localized metadata.
_generated/AzureCLIV3.versionmap.txt Updates generated V3 version map.
_generated/AzureCLIV3_Node24/azureclitask.ts Mirrors V3 Node24 runtime changes.
_generated/AzureCLIV3_Node24/src/AzureCliConfigDir.ts Mirrors V3 Node24 helper.
_generated/AzureCLIV3_Node24/Tests/L0.ts Mirrors V3 Node24 test wiring.
_generated/AzureCLIV3_Node24/Tests/L0ConfigDirIsolation.ts Mirrors V3 Node24 helper tests.
_generated/AzureCLIV3_Node24/task.json Updates V3 Node24 task version.
_generated/AzureCLIV3_Node24/task.loc.json Updates V3 Node24 localized metadata.

Comment thread Tasks/AzureCLIV1/azureclitask.ts Outdated
Comment thread Tasks/AzureCLIV2/azureclitask.ts Outdated
Comment thread Tasks/AzureCLIV3/azureclitask.ts Outdated
Commit 6626762 paired the env-var unset into
removePerInvocationAzureConfigDir, but the cleanup call ran before
logoutAzure() (which invokes `az account clear`, and on V3 also
before `az devops configure --defaults`). With AZURE_CONFIG_DIR
already unset, those `az` commands fell back to ~/.azure and mutated
the agent's global Azure CLI profile -- on self-hosted agents this
could clear unrelated cached logins.

Move the dir removal to after all `az` cleanup commands so they keep
seeing the per-invocation profile.

Addresses copilot-pull-request-reviewer[bot] review comments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@wawanawna
Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 57 out of 57 changed files in this pull request and generated 4 comments.

Comment thread Tasks/AzureCLIV1/src/AzureCliConfigDir.ts
Comment thread Tasks/AzureCLIV2/src/AzureCliConfigDir.ts
Comment thread Tasks/AzureCLIV3/src/AzureCliConfigDir.ts
Comment on lines 20 to 24
"version": {
"Major": 1,
"Minor": 274,
"Minor": 275,
"Patch": 0
},
@wawanawna
Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

…p errors

- createPerInvocationAzureConfigDir now wraps mkdtempSync in a try/catch
  and rethrows with an actionable message pointing at Agent.TempDirectory
  and the useGlobalConfig escape hatch. Behaviour is unchanged (still
  fails the task on mkdtemp failure); previously the raw EACCES/ENOENT
  was harder to diagnose.
- V2/V3: scriptType.cleanUp() failures are now logged via tl.warning
  instead of tl.debug so they show up in the build summary without
  requiring system.debug=true. Control flow is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@wawanawna
Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@wawanawna
Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

…r received HTML instead of JSON while polling child build status)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@wawanawna
Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@wawanawna
Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants