Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions docs/about/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:
Expand All @@ -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)

Expand All @@ -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

Expand Down
4 changes: 4 additions & 0 deletions docs/extend/extensibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 0 additions & 10 deletions src/IdLE.Core/Public/Assert-IdleNoScriptBlock.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 4 additions & 9 deletions src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -224,7 +219,7 @@ function New-IdleAuthSessionBroker {
$normalized = $this.DefaultAuthSession

# Validate type before returning
& $this.ValidateAuthSession -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName '<default>'
Assert-IdleAuthSessionMatchesType -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName '<default>'

return $normalized.Credential
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
76 changes: 73 additions & 3 deletions tests/Core/Assert-IdleNoScriptBlock.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,86 @@ 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' {
Expand Down
Loading