From 5f6fada8cc9b406ba1d8b10e9cbdb9dc68faf8cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:15:12 +0000 Subject: [PATCH 1/3] Initial plan From 06f1f26c8d41ccb19b3cd031d621ef1c650f9d93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:20:08 +0000 Subject: [PATCH 2/3] Remove ScriptBlock exemption for AuthSessionBroker - Remove ValidateAuthSession ScriptBlock property from broker object - Call Assert-IdleAuthSessionMatchesType directly instead of via stored ScriptBlock - Remove exemption for IdLE.AuthSessionBroker in Assert-IdleNoScriptBlock - Update tests to reflect new boundary enforcement - Add comprehensive trust boundary enforcement tests Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/about/security.md | 33 +++++--- docs/extend/extensibility.md | 4 + .../Public/Assert-IdleNoScriptBlock.ps1 | 10 --- .../Public/New-IdleAuthSessionBroker.ps1 | 13 +--- tests/Core/Assert-IdleNoScriptBlock.Tests.ps1 | 75 ++++++++++++++++++- 5 files changed, 103 insertions(+), 32 deletions(-) diff --git a/docs/about/security.md b/docs/about/security.md index b04a5d39..c159614f 100644 --- a/docs/about/security.md +++ b/docs/about/security.md @@ -11,6 +11,8 @@ Because IdLE is an orchestration engine, it must be explicit about **what is tru ## Trust boundaries +IdLE enforces a strict trust boundary between **untrusted data inputs** and **trusted extension points**. + ### Untrusted inputs (data-only) These inputs may come from users, CI pipelines, or external systems and **must be treated as untrusted**: @@ -23,16 +25,16 @@ These inputs may come from users, CI pipelines, or external systems and **must b **Rule:** Untrusted inputs must be *data-only*. They must not contain ScriptBlocks or other executable objects. -IdLE enforces this by rejecting ScriptBlocks when importing workflow definitions and by validating inputs at runtime. - -IdLE assumes these inputs are **data only**. Dynamic / executable content must be rejected. +IdLE enforces this by: +- Rejecting ScriptBlocks when importing workflow definitions +- Validating inputs at runtime using `Assert-IdleNoScriptBlock` +- Recursively scanning all hashtables, arrays, and PSCustomObjects for ScriptBlocks -Current enforcement principles: - -- Workflow definitions must be static data structures (hashtables/arrays/strings/numbers/bools). -- ScriptBlocks inside workflow definitions are rejected. -- Event sinks must be objects with a `WriteEvent(event)` method. ScriptBlock sinks are rejected. -- Step registry handlers must be **function names (strings)**. ScriptBlock handlers are rejected. +**Implementation:** +- The `Assert-IdleNoScriptBlock` function is the single, authoritative validator for this boundary +- It performs deep recursive validation with no type exemptions +- All workflow configuration, lifecycle requests, step parameters, and provider maps are validated +- Validation failures include the exact path to the offending ScriptBlock for debugging ### Trusted extension points (code) @@ -41,16 +43,27 @@ These inputs are provided by the host and are **privileged** because they determ - Step registry (maps `Step.Type` to a handler function name) - Provider modules / provider objects (system-specific adapters) - External event sinks (streaming events) +- **AuthSessionBroker** (host-provided authentication orchestration) **Rule:** Only trusted code should populate these extension points. +These extension points may contain ScriptMethods (e.g., the `AcquireAuthSession` method on AuthSessionBroker objects) but should not contain ScriptBlock *properties* that could be confused with data. + +**AuthSessionBroker Trust Model:** +- The broker is a **trusted extension point** provided by the host +- It orchestrates authentication without embedding secrets in workflows +- Broker objects may contain ScriptMethods (e.g., `AcquireAuthSession`) as part of their interface +- Broker objects must **not** contain ScriptBlock properties; all logic should be in methods or direct function calls +- Authentication options passed to `AcquireAuthSession` are validated as data-only (no ScriptBlocks) + ## Secure defaults IdLE applies secure defaults to reduce accidental code execution: - Workflow configuration is loaded as data and ScriptBlocks are rejected. -- Event streaming uses an object-based contract (`WriteEvent(event)`); ScriptBlock event sinks are rejected. - Step registry handlers must be function names (strings); ScriptBlock handlers are rejected. +- Event streaming uses an object-based contract (`WriteEvent(event)`); ScriptBlock event sinks are rejected. +- AuthSessionBroker objects should not contain ScriptBlock properties; use ScriptMethods or direct function calls instead. ## Redaction at output boundaries diff --git a/docs/extend/extensibility.md b/docs/extend/extensibility.md index ffea2a0f..21da75eb 100644 --- a/docs/extend/extensibility.md +++ b/docs/extend/extensibility.md @@ -55,6 +55,10 @@ Key points: - Options are data-only (ScriptBlocks rejected) - The broker handles caching, interactive auth policy, and secret management +**Security:** +- AuthSessionBroker is a **trusted extension point** provided by the host +- See **[Security and Trust Boundaries](../about/security.md)** for the complete trust model and ScriptBlock handling rules + For detailed contract specifications and usage patterns, see: **→ [Providers and Contracts](../extend/providers.md)** — Complete provider contracts and AuthSessionBroker details diff --git a/src/IdLE.Core/Public/Assert-IdleNoScriptBlock.ps1 b/src/IdLE.Core/Public/Assert-IdleNoScriptBlock.ps1 index 34a2fca7..c81a8b73 100644 --- a/src/IdLE.Core/Public/Assert-IdleNoScriptBlock.ps1 +++ b/src/IdLE.Core/Public/Assert-IdleNoScriptBlock.ps1 @@ -39,10 +39,6 @@ function Assert-IdleNoScriptBlock { } Assert-IdleNoScriptBlock -InputObject $data -Path 'Input' # Throws: ScriptBlocks are not allowed in request data. Found at: Input.Setting - - .NOTES - The function includes an exemption for IdLE.AuthSessionBroker objects, - which contain internal ScriptBlocks as part of their implementation. #> [CmdletBinding()] param( @@ -84,12 +80,6 @@ function Assert-IdleNoScriptBlock { # PSCustomObject (walk note properties) if ($InputObject -is [pscustomobject]) { - # Exempt trusted IdLE types that legitimately contain ScriptBlocks - # AuthSessionBroker contains ValidateAuthSession scriptblock which is an internal implementation detail - if ($InputObject.PSTypeNames -contains 'IdLE.AuthSessionBroker') { - return - } - foreach ($p in $InputObject.PSObject.Properties) { if ($p.MemberType -eq 'NoteProperty') { # PSPropertyInfo does not expose "InputObject" here; the value is in .Value. diff --git a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 index 5679405a..ba6c0a7b 100644 --- a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 +++ b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 @@ -195,16 +195,11 @@ function New-IdleAuthSessionBroker { $normalizedDefaultAuthSession = & $normalizeSessionValue $DefaultAuthSession $AuthSessionType 'DefaultAuthSession' } - # Cache the validation function for performance (avoid repeated Get-Command calls per AcquireAuthSession invocation) - $validationCommand = Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -CommandType Function -Module $MyInvocation.MyCommand.Module -ErrorAction Stop - $validationScriptBlock = $validationCommand.ScriptBlock - $broker = [pscustomobject]@{ PSTypeName = 'IdLE.AuthSessionBroker' SessionMap = $normalizedSessionMap DefaultAuthSession = $normalizedDefaultAuthSession AuthSessionType = $AuthSessionType - ValidateAuthSession = $validationScriptBlock } $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { @@ -224,7 +219,7 @@ function New-IdleAuthSessionBroker { $normalized = $this.DefaultAuthSession # Validate type before returning - & $this.ValidateAuthSession -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName '' + Assert-IdleAuthSessionMatchesType -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName '' return $normalized.Credential } @@ -237,7 +232,7 @@ function New-IdleAuthSessionBroker { $normalized = $this.DefaultAuthSession # Validate type before returning - & $this.ValidateAuthSession -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name + Assert-IdleAuthSessionMatchesType -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name return $normalized.Credential } @@ -324,7 +319,7 @@ function New-IdleAuthSessionBroker { $normalized = $matchingEntries[0].Value # Validate type before returning - & $this.ValidateAuthSession -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name + Assert-IdleAuthSessionMatchesType -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name return $normalized.Credential } @@ -344,7 +339,7 @@ function New-IdleAuthSessionBroker { $normalized = $this.DefaultAuthSession # Validate type before returning - & $this.ValidateAuthSession -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name + Assert-IdleAuthSessionMatchesType -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name return $normalized.Credential } diff --git a/tests/Core/Assert-IdleNoScriptBlock.Tests.ps1 b/tests/Core/Assert-IdleNoScriptBlock.Tests.ps1 index 94214954..13178531 100644 --- a/tests/Core/Assert-IdleNoScriptBlock.Tests.ps1 +++ b/tests/Core/Assert-IdleNoScriptBlock.Tests.ps1 @@ -151,16 +151,85 @@ Describe 'Assert-IdleNoScriptBlock' { } } - Context 'Trusted type exemptions' { - It 'allows IdLE.AuthSessionBroker with internal ScriptBlock' { + Context 'Trust boundary enforcement' { + It 'rejects IdLE.AuthSessionBroker with ScriptBlock properties' { + # AuthSessionBroker should no longer have ScriptBlock exemption + # Any ScriptBlock properties in broker objects should be rejected $broker = [pscustomobject]@{ PSTypeName = 'IdLE.AuthSessionBroker' - ValidateAuthSession = { param($s) return $true } + MaliciousProperty = { Write-Host "malicious code" } + } + $broker.PSObject.TypeNames.Insert(0, 'IdLE.AuthSessionBroker') + + { Assert-IdleNoScriptBlock -InputObject $broker -Path 'Broker' } | + Should -Throw -ExceptionType ([System.ArgumentException]) -ExpectedMessage '*ScriptBlocks are not allowed*Broker.MaliciousProperty*' + } + + It 'allows IdLE.AuthSessionBroker without ScriptBlock properties' { + # Broker objects created by New-IdleAuthSessionBroker should pass validation + $broker = [pscustomobject]@{ + PSTypeName = 'IdLE.AuthSessionBroker' + SessionMap = @{} + DefaultAuthSession = $null + AuthSessionType = 'Credential' } $broker.PSObject.TypeNames.Insert(0, 'IdLE.AuthSessionBroker') { Assert-IdleNoScriptBlock -InputObject $broker -Path 'Broker' } | Should -Not -Throw } + + It 'rejects workflow configuration with ScriptBlock' { + $workflow = @{ + Name = 'TestWorkflow' + LifecycleEvent = 'Joiner' + Steps = @( + @{ + Name = 'Step1' + Type = 'Test.Step' + With = @{ + Action = { Invoke-MaliciousCode } + } + } + ) + } + + { Assert-IdleNoScriptBlock -InputObject $workflow -Path 'Workflow' } | + Should -Throw -ExceptionType ([System.ArgumentException]) + + try { + Assert-IdleNoScriptBlock -InputObject $workflow -Path 'Workflow' + } + catch { + $_.Exception.Message | Should -Match 'Workflow\.Steps\[0\]\.With\.Action' + } + } + + It 'rejects provider map with ScriptBlock' { + $providerMap = @{ + Identity = [pscustomobject]@{ + Provider = 'MockProvider' + MaliciousCallback = { Write-Host "malicious" } + } + } + + { Assert-IdleNoScriptBlock -InputObject $providerMap -Path 'ProviderMap' } | + Should -Throw -ExceptionType ([System.ArgumentException]) -ExpectedMessage '*ScriptBlocks are not allowed*' + } + + It 'rejects lifecycle request with ScriptBlock in DesiredState' { + $request = @{ + LifecycleEvent = 'Joiner' + CorrelationId = 'test-123' + DesiredState = @{ + Identity = @{ + BadProperty = { Get-Credential } + } + } + } + + { Assert-IdleNoScriptBlock -InputObject $request -Path 'Request' } | + Should -Throw -ExceptionType ([System.ArgumentException]) -ExpectedMessage '*ScriptBlocks are not allowed*Request.DesiredState.Identity.BadProperty*' + } } Context 'Path reporting' { From 41ea152c5df521fbe07921bbbe196698543431eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:21:50 +0000 Subject: [PATCH 3/3] Address code review: improve test formatting - Format multi-line assertion for better readability - Use explicit code block for improved clarity Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- tests/Core/Assert-IdleNoScriptBlock.Tests.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Core/Assert-IdleNoScriptBlock.Tests.ps1 b/tests/Core/Assert-IdleNoScriptBlock.Tests.ps1 index 13178531..9944c117 100644 --- a/tests/Core/Assert-IdleNoScriptBlock.Tests.ps1 +++ b/tests/Core/Assert-IdleNoScriptBlock.Tests.ps1 @@ -161,8 +161,9 @@ Describe 'Assert-IdleNoScriptBlock' { } $broker.PSObject.TypeNames.Insert(0, 'IdLE.AuthSessionBroker') - { Assert-IdleNoScriptBlock -InputObject $broker -Path 'Broker' } | - Should -Throw -ExceptionType ([System.ArgumentException]) -ExpectedMessage '*ScriptBlocks are not allowed*Broker.MaliciousProperty*' + { + Assert-IdleNoScriptBlock -InputObject $broker -Path 'Broker' + } | Should -Throw -ExceptionType ([System.ArgumentException]) -ExpectedMessage '*ScriptBlocks are not allowed*Broker.MaliciousProperty*' } It 'allows IdLE.AuthSessionBroker without ScriptBlock properties' {