Common/Sanitizer: allow PowerShell data constructors @ { } [ ] - #22253
Closed
Vladimir Tretyakov (wawanawna) wants to merge 8 commits into
Closed
Common/Sanitizer: allow PowerShell data constructors @ { } [ ]#22253Vladimir Tretyakov (wawanawna) wants to merge 8 commits into
Vladimir Tretyakov (wawanawna) wants to merge 8 commits into
Conversation
The PowerShell ArgumentsSanitizer regex blocked benign data syntax
(`-Tag @{...}`, splat `@params`, type accelerator `[string]`, literal
index `[0]`) that customers commonly pass via scriptArguments.
PR #22171 wired this sanitizer into AzurePowerShellV2-V5 and
ServiceFabricPowerShellV1 behind two FFs
(AZP_75787_ENABLE_NEW_LOGIC + DistributedTask.Tasks.EnableAzure
PowerShellArgumentsSanitization). Once the per-org FF flips on,
every pipeline using the syntax above would fail with
PS_ScriptArgsSanitized - exactly the regression Ivan just fixed
for AzureCLI in PR #22249 (issue #22173).
Adds `@ { } [ ]` to the allowed character class. Attack primitives
remain blocked: `; & | $ ( )` and unescaped backtick. Mirrors Ivan's
TS-side regex change in Tasks/AzureCLIV{2,3}/src/argsSanitizer.ts
exactly.
Tests:
- New L0Protect-ScriptArguments.AllowsDataConstructors.ps1 (7 cases:
hashtable, multi-line hashtable, splat, type accelerator, index,
hashtable with email, hashtable with $env: substitution).
- L0Protect-ScriptArguments.Throws.ps1: +5 must-still-throw cases
combining allowed data syntax with attack primitives (hashtable
hiding $(Get-Date), $(rm -rf /), `& evil.exe`, array literal
`@(...)`, hashtable with `;` separator).
- L0Get-SanitizedArgumentsArray.DoesNotBreakExistingPowerShellFormats:
+4 identity-preservation cases confirming Get-SanitizedArgumentsArray
doesn't mangle the newly-allowed syntax.
- L0.ts wired the new file as a separate it() block.
- All 9 L0 suites pass locally (mocha 3s).
Bumps Patch on all 17 consumer tasks + 5 _generated mirrors per the
PR #22171 precedent (AzureFileCopyV1-V6, AzurePowerShellV2-V5,
PowerShellOnTargetMachinesV3, PowerShellV2, ServiceFabricPowerShellV1,
SqlAzureDacpacDeploymentV1, SqlDacpacDeploymentOnMachineGroupV0,
WindowsMachineFileCopyV1/V2).
Vladimir Tretyakov (wawanawna)
requested review from
a team,
Razvan Manole (manolerazvan) and
Tarun Ramsinghani (tarunramsinghani)
as code owners
June 12, 2026 13:27
Copilot started reviewing on behalf of
Vladimir Tretyakov (wawanawna)
June 12, 2026 13:31
View session
Contributor
There was a problem hiding this comment.
Pull request overview
Updates the shared PowerShell scriptArguments sanitizer in Tasks/Common/Sanitizer to allow common PowerShell data-constructor syntax characters (@ { } [ ]) so benign hashtables/splatting/type accelerators don’t get rejected when the sanitizer feature flags are enabled, and rolls the change out via patch version bumps to all consumer tasks (including _generated mirrors and version maps).
Changes:
- Relax
ArgumentsSanitizer.ps1allowlist regex to permit@ { } [ ]in non-execution contexts. - Add/extend L0 tests to cover allowed data-constructors and must-still-block attack primitives.
- Patch-bump all sanitizer consumers (and update
_generated/*+*.versionmap.txtwhere applicable).
Reviewed changes
Copilot reviewed 64 out of 64 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| Tasks/Common/Sanitizer/ArgumentsSanitizer.ps1 | Allowlist regex updated to permit @ { } [ ]; inline documentation updated. |
| Tasks/Common/Sanitizer/Tests/L0Protect-ScriptArguments.AllowsDataConstructors.ps1 | New L0 test file covering permitted data-constructor patterns. |
| Tasks/Common/Sanitizer/Tests/L0Protect-ScriptArguments.Throws.ps1 | Adds negative cases ensuring execution primitives remain blocked. |
| Tasks/Common/Sanitizer/Tests/L0Get-SanitizedArgumentsArray.DoesNotBreakExistingPowerShellFormats.ps1 | Adds identity-preservation cases for newly allowed patterns. |
| Tasks/Common/Sanitizer/Tests/L0.ts | Registers the new L0 test file in the mocha suite. |
| Tasks/AzureFileCopyV1/task.json | Patch version bump (deprecated task). |
| Tasks/AzureFileCopyV1/task.loc.json | Patch version bump. |
| Tasks/AzureFileCopyV2/task.json | Patch version bump (deprecated task). |
| Tasks/AzureFileCopyV2/task.loc.json | Patch version bump. |
| Tasks/AzureFileCopyV3/task.json | Patch version bump (deprecated task). |
| Tasks/AzureFileCopyV3/task.loc.json | Patch version bump. |
| Tasks/AzureFileCopyV4/task.json | Patch version bump. |
| Tasks/AzureFileCopyV4/task.loc.json | Patch version bump. |
| Tasks/AzureFileCopyV5/task.json | Patch version bump. |
| Tasks/AzureFileCopyV5/task.loc.json | Patch version bump. |
| Tasks/AzureFileCopyV6/task.json | Patch version bump. |
| Tasks/AzureFileCopyV6/task.loc.json | Patch version bump. |
| Tasks/AzurePowerShellV2/task.json | Patch version bump (deprecated task). |
| Tasks/AzurePowerShellV2/task.loc.json | Patch version bump. |
| Tasks/AzurePowerShellV3/task.json | Patch version bump (deprecated task). |
| Tasks/AzurePowerShellV3/task.loc.json | Patch version bump. |
| Tasks/AzurePowerShellV4/task.json | Patch version bump. |
| Tasks/AzurePowerShellV4/task.loc.json | Patch version bump. |
| Tasks/AzurePowerShellV5/task.json | Patch version bump. |
| Tasks/AzurePowerShellV5/task.loc.json | Patch version bump. |
| Tasks/PowerShellV2/task.json | Patch version bump. |
| Tasks/PowerShellV2/task.loc.json | Patch version bump. |
| Tasks/PowerShellOnTargetMachinesV3/task.json | Patch version bump. |
| Tasks/PowerShellOnTargetMachinesV3/task.loc.json | Patch version bump. |
| Tasks/ServiceFabricPowerShellV1/task.json | Patch version bump. |
| Tasks/ServiceFabricPowerShellV1/task.loc.json | Patch version bump. |
| Tasks/SqlAzureDacpacDeploymentV1/task.json | Patch version bump. |
| Tasks/SqlAzureDacpacDeploymentV1/task.loc.json | Patch version bump. |
| Tasks/SqlDacpacDeploymentOnMachineGroupV0/task.json | Patch version bump. |
| Tasks/SqlDacpacDeploymentOnMachineGroupV0/task.loc.json | Patch version bump. |
| Tasks/WindowsMachineFileCopyV1/task.json | Patch version bump. |
| Tasks/WindowsMachineFileCopyV1/task.loc.json | Patch version bump. |
| Tasks/WindowsMachineFileCopyV2/task.json | Patch version bump. |
| Tasks/WindowsMachineFileCopyV2/task.loc.json | Patch version bump. |
| _generated/AzureFileCopyV4/task.json | Mirror patch version bump. |
| _generated/AzureFileCopyV4/task.loc.json | Mirror patch version bump. |
| _generated/AzureFileCopyV4.versionmap.txt | Version map updated. |
| _generated/AzureFileCopyV4_Node24/task.json | Node24 mirror patch version bump. |
| _generated/AzureFileCopyV4_Node24/task.loc.json | Node24 mirror patch version bump. |
| _generated/AzureFileCopyV5/task.json | Mirror patch version bump. |
| _generated/AzureFileCopyV5/task.loc.json | Mirror patch version bump. |
| _generated/AzureFileCopyV5.versionmap.txt | Version map updated. |
| _generated/AzureFileCopyV5_Node24/task.json | Node24 mirror patch version bump. |
| _generated/AzureFileCopyV5_Node24/task.loc.json | Node24 mirror patch version bump. |
| _generated/AzureFileCopyV6/task.json | Mirror patch version bump. |
| _generated/AzureFileCopyV6/task.loc.json | Mirror patch version bump. |
| _generated/AzureFileCopyV6.versionmap.txt | Version map updated. |
| _generated/AzureFileCopyV6_Node24/task.json | Node24 mirror patch version bump. |
| _generated/AzureFileCopyV6_Node24/task.loc.json | Node24 mirror patch version bump. |
| _generated/AzurePowerShellV4/task.json | Mirror patch version bump. |
| _generated/AzurePowerShellV4/task.loc.json | Mirror patch version bump. |
| _generated/AzurePowerShellV4.versionmap.txt | Version map updated. |
| _generated/AzurePowerShellV4_Node24/task.json | Node24 mirror patch version bump. |
| _generated/AzurePowerShellV4_Node24/task.loc.json | Node24 mirror patch version bump. |
| _generated/AzurePowerShellV5/task.json | Mirror patch version bump. |
| _generated/AzurePowerShellV5/task.loc.json | Mirror patch version bump. |
| _generated/AzurePowerShellV5.versionmap.txt | Version map updated. |
| _generated/AzurePowerShellV5_Node24/task.json | Node24 mirror patch version bump. |
| _generated/AzurePowerShellV5_Node24/task.loc.json | Node24 mirror patch version bump. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Contributor
Author
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
…d task versions CI failure: BuildConfigGen verify step rejected the previous commit because the inner `_buildConfigMapping` block at the bottom of each generated `task.json` / `task.loc.json` still referenced the master baseline versions (`.0` for Default, `.1` for Node24_1). The top-level `version` field was already bumped in the original PR, but the embedded mapping block was missed for the 5 families that have a Node24 build config. Reproduced locally with: dotnet run --no-launch-profile --project BuildConfigGen/BuildConfigGen.csproj --task `...` --write-updates Re-verify (no --write-updates) now exits 0. Affected families (Default + Node24 mirrors of each): - AzureFileCopyV4 / V5 / V6 - AzurePowerShellV4 / V5 20 files changed; only the inner `Default` / `Node24_1` mapping entries shift +1. No source task changes, no behaviour changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
Author
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
CI failure: `Check for downgrading tasks` step rejected the previous bump because the rule is `new Default > max(all previous configs)`, not just `> previous Default`. For families with a Node24_1 mirror, the prior max was Default+1, so source bump +1 left new Default equal to prior Node24_1. Fix: bump source `Tasks/<X>/task.json` and `task.loc.json` Patch `.1 -> .3` for the 5 affected families. BuildConfigGen regenerates Default=.3 / Node24_1=.4, both strictly above prior max .1, satisfying both `Verify generated files` and `Check for downgrading tasks`. Affected families: - AzureFileCopyV4 / V5 / V6 - AzurePowerShellV4 / V5 35 files changed: 10 source + 5 versionmap + 20 generated. No behavioural changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
Author
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
Walks back the previous over-bump. The downgrade check in ci/check-downgrading.js
requires only `new > master max`, not `master max + 2`. For these 5 families
where prior master had Default=.276.0 and Node24_1=.276.1 (so max=.276.1),
the minimum valid bump is Default=.276.2 / Node24_1=.276.3 — not .276.3/.276.4.
Source `Tasks/<X>/{task.json,task.loc.json}` Patch 3 -> 2.
Generated files and versionmap updated in lockstep.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
Author
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
Nemanja Rogic (nemanjarogic)
approved these changes
Jun 15, 2026
…pecial-characters
Contributor
Author
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
Vladimir Tretyakov (wawanawna)
pushed a commit
that referenced
this pull request
Jun 17, 2026
PR #22253 proposed relaxing the PowerShell argument-sanitizer allow-list to permit the data-constructor characters @ { } [ ] (regression issue #22173) so legitimate hashtable/array arguments to AzurePowerShell and ServiceFabricPowerShell scripts are no longer mangled. That character-only relaxation, however, re-opens code execution: the sanitizer only *validates* arguments - the task runs the RAW string verbatim at a dot-source sink (". '<script>' <args>" / "& '<script>' <args>"), so an argument such as -Tag @{ k = New-Item -Path C:\evil.txt -ItemType File -Force } -Tag @{ k = $(whoami) } -Tag @{ k = [adsi]'LDAP://attacker' } uses only allow-listed characters yet EXECUTES, because a hashtable value, array element, sub-expression or cast inside a data constructor is an evaluated expression at bind time. (The same tokens at top-level argument position are inert literal strings at this sink - verified empirically against both real sinks.) PR #22249 already shipped the equivalent relaxation for AzureCLI and carries the same exposure. Fix: keep the relaxed character allow-list but add a structural AST backstop, Test-SanitizerArgumentAst, that parses the raw arguments exactly as the sink does and rejects anything that is not a pure data literal - parse errors, nested commands, script blocks, member access / method calls and type casts - while still allowing @{ Port = 8080 }, @{ Owner = "a@b.com" }, splatting, $env: variables, quoted strings and numbers. The relaxed path is opt-in via a new -AllowDataConstructors switch on Get-SanitizedArguments / Protect-ScriptArguments. Only the dispatcher (Invoke-ScriptArgumentSanitization, used by AzurePowerShellV2-V5 and ServiceFabricPowerShellV1) passes it. Legacy direct callers (AzureFileCopy, PowerShell, PowerShellOnTargetMachines, WindowsMachineFileCopy, Sql*Deployment) keep the strict allow-list with no behavioral change (the switch defaults off). L0 coverage added: - Test-SanitizerArgumentAst classification (data literals vs. executable exprs) - relaxed path allows legitimate data constructors - relaxed path blocks data-constructor injection, with an explicit assertion that the character regex alone does NOT catch them (the AST is load-bearing) - strict (legacy) path is unchanged for non-dispatcher callers Version-bumped all 17 sanitizer-bundling tasks and regenerated _generated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
3 tasks
Vladimir Tretyakov (wawanawna)
marked this pull request as draft
June 19, 2026 13:01
Vladimir Tretyakov (wawanawna)
added a commit
that referenced
this pull request
Jun 23, 2026
…22173, supersedes #22253) (#22273) * Harden PowerShell argument sanitizer against data-constructor RCE PR #22253 proposed relaxing the PowerShell argument-sanitizer allow-list to permit the data-constructor characters @ { } [ ] (regression issue #22173) so legitimate hashtable/array arguments to AzurePowerShell and ServiceFabricPowerShell scripts are no longer mangled. That character-only relaxation, however, re-opens code execution: the sanitizer only *validates* arguments - the task runs the RAW string verbatim at a dot-source sink (". '<script>' <args>" / "& '<script>' <args>"), so an argument such as -Tag @{ k = New-Item -Path C:\evil.txt -ItemType File -Force } -Tag @{ k = $(whoami) } -Tag @{ k = [adsi]'LDAP://attacker' } uses only allow-listed characters yet EXECUTES, because a hashtable value, array element, sub-expression or cast inside a data constructor is an evaluated expression at bind time. (The same tokens at top-level argument position are inert literal strings at this sink - verified empirically against both real sinks.) PR #22249 already shipped the equivalent relaxation for AzureCLI and carries the same exposure. Fix: keep the relaxed character allow-list but add a structural AST backstop, Test-SanitizerArgumentAst, that parses the raw arguments exactly as the sink does and rejects anything that is not a pure data literal - parse errors, nested commands, script blocks, member access / method calls and type casts - while still allowing @{ Port = 8080 }, @{ Owner = "a@b.com" }, splatting, $env: variables, quoted strings and numbers. The relaxed path is opt-in via a new -AllowDataConstructors switch on Get-SanitizedArguments / Protect-ScriptArguments. Only the dispatcher (Invoke-ScriptArgumentSanitization, used by AzurePowerShellV2-V5 and ServiceFabricPowerShellV1) passes it. Legacy direct callers (AzureFileCopy, PowerShell, PowerShellOnTargetMachines, WindowsMachineFileCopy, Sql*Deployment) keep the strict allow-list with no behavioral change (the switch defaults off). L0 coverage added: - Test-SanitizerArgumentAst classification (data literals vs. executable exprs) - relaxed path allows legitimate data constructors - relaxed path blocks data-constructor injection, with an explicit assertion that the character regex alone does NOT catch them (the AST is load-bearing) - strict (legacy) path is unchanged for non-dispatcher callers Version-bumped all 17 sanitizer-bundling tasks and regenerated _generated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Block -as conversion operator and bare type references in AST backstop Independent security review found a bypass: an -as conversion such as @{ k = 'C:\victim\file.ps1' -as [System.IO.StreamWriter] } uses only allow-listed characters and parses as a BinaryExpressionAst + TypeExpressionAst (neither flagged by the previous predicate), but still invokes the target type's constructor at the dot-source sink - verified to execute with both a [type] literal and a string/variable right operand, on both the AzurePowerShell and ServiceFabric sinks under PowerShell 5.1 and 7. Test-SanitizerArgumentAst now also rejects: - the -as operator (BinaryExpressionAst with TokenKind.As), regardless of the right-operand form, which also makes the [type]'x' cast and 'x' -as [type] conversion forms consistently blocked; and - any TypeExpressionAst (a type reference used as a value inside a data constructor); top-level type literals passed as plain arguments do not parse as TypeExpressionAst and remain allowed (e.g. -Type [System.String]). L0: added -as (type-literal and value-type) and bare-type-reference cases to the classifier and integration injection corpora; -Type [System.String] added to the allow corpus. Full Sanitizer L0 suite passes (13/13). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clarify comments: @(...) array literals stay blocked (parens not allowed) * Polish sanitizer comments: strict vs relaxed modes, two task groups, unify goal * Refactor: shared regex base + module-level dangerous AST type list * Revert to two explicit regex literals for readability * Bump AzurePowerShellV4/V5 above master (resolve common-change downgrade) + fix typo * Fix AzurePowerShellV4 task.json version (racy-clean miss in e64787a: .2->.4 same byte-length) The previous commit updated V4's _generated versionmap to 4.276.4 but git's stat cache missed the same-byte-length task.json edit (.2->.4), leaving the committed task.json at 4.276.2. BuildConfigGen rejected the inconsistency (inputVersion 4.276.2 <= maxVersion 4.276.5, != defaultVersion 4.276.4). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Restore AZP_75787_ENABLE_NEW_LOGIC after data-constructor L0 tests Addresses review feedback: the two new L0 tests set the AZP_75787_ENABLE_NEW_LOGIC feature-flag env var at load time but never restored it. The L0 PSRunner reuses a single powershell.exe process across test scripts, so the unrestored value leaked into subsequent tests. Capture the original value and restore it (or remove it when originally unset) in a finally block. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Restore AZP_75787_ENABLE_NEW_LOGIC in pre-existing Passes/Throws L0 tests Same env-var leak as the new data-constructor tests: these two pre-existing siblings set AZP_75787_ENABLE_NEW_LOGIC at load time and never restored it, leaking 'true' into subsequent tests through the shared PSRunner process. Wrap them in the same try/finally restore so the full Sanitizer L0 suite runs leak-free. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Uladzimir Tratsiakou (Vladimir/Vova) <utratsiakou@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Customer-impact PR. PR #22171 wired the existing
Common/Sanitizer/ArgumentsSanitizer.ps1validator into the AzurePowerShellV2-V5 and ServiceFabricPowerShellV1 tasks behind two feature flags (AZP_75787_ENABLE_NEW_LOGICorg-level +DistributedTask.Tasks.EnableAzurePowerShellArgumentsSanitizationper-pipeline). The regex allow-list in that validator does not include@,{,},[,], so customers using benign PowerShell data syntax inscriptArgumentswould hitPS_ScriptArgsSanitizedthe moment the per-org FF is enabled — identical to the regression Ivan just fixed for the AzureCLI side in #22249 (issue #22173).Mirrors:
Tasks/AzureCLIV{2,3}/src/argsSanitizer.tsRelated context:
$env:env-var terminator fixTask Name
Common/Sanitizerplus version bumps on all 17 consumer tasks:AzureFileCopyV1-V6(6),AzurePowerShellV2-V5(4),PowerShellOnTargetMachinesV3,PowerShellV2,ServiceFabricPowerShellV1,SqlAzureDacpacDeploymentV1,SqlDacpacDeploymentOnMachineGroupV0,WindowsMachineFileCopyV1/V2(2) — matches the consumer set bumped in PR #22171.Description
Tasks/Common/Sanitizer/ArgumentsSanitizer.ps1: the negated character class now also allows@,{,},[,]. Concretely:'(?<!)([^\w\_''"\-=\/:\.*,+~?%\n#])(?!true|false)''(?<!)([^\w\_''"\-=\/:\.*,+~?%\n#@{}\[\]])(?!true|false)'These five characters by themselves cannot construct an executable PowerShell expression. Attack primitives stay blocked:
;,&,|,$,(,), and unescaped backtick. Mirrors Ivan's TS regex change inTasks/AzureCLIV{2,3}/src/argsSanitizer.tsexactly.Customer-visible:
-Tag @{...}, multi-line hashtables, splat@params, type accelerator[string], literal index[0], and hashtables that quote$env:Foosubstitutions now pass when the FF is on. Without this change the same customer-impact wave the AzureCLI side just took would land on PowerShell tasks the moment the org FF flips.Risk Assessment (Low / Medium / High)
Low. Strictly relaxes the allow-list — no new characters become executable on their own, and every attack primitive (
; & | $ ( )and unescaped backtick) is still blocked. Behavior when the FF is off is unchanged. Identity-preservation tests confirmGet-SanitizedArgumentsArraydoes not mangle the newly-allowed input.Change Behind Feature Flag (Yes / No)
Yes. Reuses the existing two-flag gating from PR #22171:
AZP_75787_ENABLE_NEW_LOGIC(org) andDistributedTask.Tasks.EnableAzurePowerShellArgumentsSanitization(per-pipeline). This PR does not change FF wiring — only the regex inside the validator that runs once both flags are On.Tech Design / Approach
The TS-side fix in #22249 is the reference implementation. The PS regex now allows the same set of characters Ivan added on the TS side, so customer-facing behavior is symmetric across language flavors of the task family. Alternative considered: allow only
@and{}(covers@{...}and splat) but reject[]. Rejected —[string]type accelerator and literal index[0]are equally idiomatic inscriptArguments, and once$()are blocked the index/accelerator cannot reach an evaluator.Documentation Changes Required (Yes/No)
No.
Unit Tests Added or Updated (Yes / No)
Yes.
Tasks/Common/Sanitizer/Tests/L0Protect-ScriptArguments.AllowsDataConstructors.ps1— 7 positive cases covering hashtable, multi-line hashtable, splat@params, type accelerator[string], literal index[0], hashtable with email value, and hashtable with$env:substitution. Uses$test.Inputcorrectly (siblingPasses.ps1has a latent$inputArgstypo that silently no-ops the loop — out of scope to fix here).Tasks/Common/Sanitizer/Tests/L0Protect-ScriptArguments.Throws.ps1— +5 must-still-throw cases combining allowed data syntax with attack primitives: hashtable hiding$(Get-Date),$(rm -rf /),-Cmd & evil.exe, array literal@("a","b","c"), hashtable with;separator.Tasks/Common/Sanitizer/Tests/L0Get-SanitizedArgumentsArray.DoesNotBreakExistingPowerShellFormats.ps1— +4 identity-preservation cases for hashtable, splat, type accelerator, index.L0.tswired the new file as a separateit()block.Result: full Common/Sanitizer L0 suite (9
it()blocks) passes locally on Node 22.17.1 / mocha.Additional Testing Performed
Protect-ScriptArgumentswithGet-VstsLocString/Write-VstsTaskWarningmocked: 13/13 hand-picked cases pass — 5 positive (hashtable, splat,[string],[0], hashtable with@in value) + 8 negative (semi,&&,$(),|, hashtable+subexpr, hashtable+;, array literal@(..),& call).Tasks/Common/Sanitizer/Testsrun end-to-end (build vianode make.js build --task WindowsMachineFileCopyV1, thenmocha ./L0.js): 9 passing.tfspf-rmpf/MSRCTestfor the original MSRC vector (ICM 115118): confirmed the sanitizer (when the FF is enabled) blocks the attack inputs unchanged.Logging Added/Updated (Yes/No)
No.
Telemetry Added/Updated (Yes/No)
No. The existing
sanitizerThrew/OrgLevelFeatureFlagtelemetry continues to fire unchanged.Rollback Scenario and Process (Yes/No)
Yes. The per-org
AZP_75787_ENABLE_NEW_LOGICand per-pipelineDistributedTask.Tasks.EnableAzurePowerShellArgumentsSanitizationflags can disable the entire sanitizer if a regression emerges. A regex revert inTasks/Common/Sanitizer/ArgumentsSanitizer.ps1is a one-line change. Patch-only version bumps mean the task minors do not move.Dependency Impact Assessed and Regression Tested (Yes/No)
Yes. Same impact set as PR #22171: 17 consumer tasks listed under Task Name. All are bumped here in lockstep (Patch +1).
_generated/mirrors for AzureFileCopyV4/V5/V6 and AzurePowerShellV4/V5 (Default +_Node24variants) plus their.versionmap.txtfiles are bumped in lockstep with their non-_generated/source. No public API contract changes.Checklist