Skip to content

Constrained Language Mode profiles for SPE remoting security #1426

@michaellwest

Description

@michaellwest

Constrained Language Mode (CLM) for SPE Remoting

Summary

SPE remoting previously ran all remote scripts in FullLanguage mode with no granular control over what commands or modules a remote caller could use. This issue tracks the implementation of Constrained Language Mode (CLM) support, bringing defense-in-depth to SPE's remoting surface.

The core implementation is complete across 11 commits on feature/clm. What remains is documentation and a migration guide.

What was built:

  • Restriction Profiles -- named security policies that control language mode, command access, module access, item path access, and audit level
  • Item path restrictions -- profiles can deny or allow access to Sitecore content tree paths (prefix match), protecting sensitive items like API Keys from remote scripts
  • Item-based profile overrides -- Sitecore content items that extend config-defined profiles with additional restrictions or allowances
  • Trusted Scripts -- a binary trust model where scripts must be explicitly trusted (by item) to run under restricted profiles
  • Remoting API Keys -- item-based API key management with per-key profile binding, rate limiting, and throttling
  • Fail-closed security -- unknown profiles resolve to DenyAll, dynamic invocations are rejected in blocklist mode, timing-safe secret comparison throughout

Restriction Profiles

A Restriction Profile defines the security policy applied to a remote session. Profiles are defined in Sitecore config and can be extended via content items.

Config-defined profiles (4 predefined):

Profile Language Mode Command Policy Item Path Policy Description
unrestricted FullLanguage No restrictions No restrictions Default -- backward compatible
read-only ConstrainedLanguage Blocklist (write commands blocked) Blocklist (Remoting settings denied) Safe for reporting
read-only-strict ConstrainedLanguage Allowlist (only approved commands) Blocklist (Remoting settings denied) Locked-down reporting
content-editor ConstrainedLanguage Blocklist (dangerous commands blocked) Allowlist (content, media, layout only) Content operations only

Profile properties:

  • Language Mode -- FullLanguage or ConstrainedLanguage
  • Command Restrictions -- blocklist or allowlist of PowerShell commands
  • Module Restrictions -- which modules may be loaded
  • Item Path Restrictions -- blocklist or allowlist of Sitecore content tree paths (prefix match)
  • Audit Level -- controls logging verbosity
  • Enforcement Mode -- active enforcement or audit-only

Item-based profile overrides

Content items under /sitecore/system/Modules/PowerShell/Settings/Remoting/Restriction Profiles/ can extend a config-defined base profile. Overrides are additive only -- they can add blocked or allowed commands/paths but cannot remove base restrictions.

  • Template: Restriction Profile (under /templates/Modules/PowerShell Console/Remoting/)
  • Fields: Base Profile, Additional Blocked Commands, Additional Allowed Commands, Additional Blocked Paths (Treelist), Additional Allowed Paths (Treelist), Audit Level Override, Enabled
  • Caching: HttpRuntime.Cache with TTL-based expiry

Profile resolution order:

  1. JWT scope claim (if present)
  2. API Key item profile (if API key auth used)
  3. Service-level config
  4. unrestricted (fallback)

Unknown profile references resolve to DenyAll (fail closed).


Item Path Restrictions

Profiles can restrict which Sitecore items remote scripts can access. This prevents constrained callers from reading sensitive items like API Keys, trust configuration, or other protected content tree areas.

How it works:

  • Each profile has an optional itemPathRestrictions section with a mode (blocklist or allowlist) and a list of Sitecore paths
  • Prefix matching -- blocking /sitecore/system/Modules/PowerShell/Settings/Remoting also blocks all children
  • Enforcement happens in the Sitecore provider (PsSitecoreItemProvider) at the WriteItem() and GetChildNames() egress points
  • Catches all access patterns: by path, by ID/GUID, by query, and via Get-ChildItem

Default restrictions:

Profile Mode Paths
unrestricted none --
read-only blocklist /sitecore/system/Modules/PowerShell/Settings/Remoting
read-only-strict blocklist /sitecore/system/Modules/PowerShell/Settings/Remoting
content-editor allowlist /sitecore/content, /sitecore/media library, /sitecore/layout

Config example:

<profile name="read-only" ...>
  <itemPathRestrictions mode="blocklist">
    <blockedPaths>
      <path>/sitecore/system/Modules/PowerShell/Settings/Remoting</path>
    </blockedPaths>
  </itemPathRestrictions>
</profile>

Item-based path overrides:

Override items can add paths via Treelist fields (Additional Blocked Paths, Additional Allowed Paths). Treelist stores item GUIDs, so restrictions survive item renames and moves. GUIDs are resolved to paths at merge time.

Enforcement:

  • The active RestrictionProfile is set on ScriptSession.ActiveRestrictionProfile by the remoting handler before script execution
  • The provider reads this via the $ScriptSession PowerShell variable
  • Non-remoting contexts (ISE, console) have no active profile and are unrestricted
  • Respects Enforcement mode: Enforce blocks access with a non-terminating error, Audit only logs
  • Respects AuditLevel for logging denied access attempts

Known limitation:

  • Provider-level enforcement only. Direct .NET API calls like item.Children from within a script are not restricted. This is acceptable because ConstrainedLanguage mode already restricts arbitrary method invocation in constrained profiles.

Trusted Scripts

Trust is binary -- a script is either trusted or untrusted. There are no trust levels.

Trusted scripts are managed entirely through content items under /sitecore/system/Modules/PowerShell/Settings/Remoting/Trusted Scripts/. Each trust item references one or more scripts via a Treelist field, so a single trust item can cover a group of related scripts.

Trust item properties:

  • Script -- Treelist field referencing PowerShell Script Library items
  • Allowed Profiles -- restricts which restriction profiles the trust applies to
  • Enabled -- checkbox for easy toggling without deleting the item

How trust is evaluated:

  • ScriptTrustRegistry.EvaluateTrust() is called during RemoteScriptCall execution
  • Trust is checked for both REST and SOAP (RemoteAutomation.asmx) remoting endpoints
  • Untrusted scripts are rejected under restricted profiles
  • Cache is invalidated automatically via TrustedScriptSaveHandler when trust items are saved

Design rationale:

  • Content hash verification was dropped for item-based trust -- the admin who can edit a script can also edit the trust item, making hash checks redundant
  • Config-based trust (XML trustedScripts, scopeRestrictions, Generate-TrustedScripts.ps1) was removed entirely in favor of the simpler item-based approach

Remoting API Keys

API keys are now managed as Sitecore content items instead of relying solely on a single shared secret in config.

API Key item properties:

  • Shared Secret -- the authentication secret for this key
  • Enabled -- checkbox to activate/deactivate
  • Profile -- which restriction profile applies to sessions using this key
  • Impersonate User -- optional user context for the remote session
  • Request Limit -- maximum requests allowed within the throttle window
  • Throttle Window -- time window for rate limiting

Provider behavior:

  • RemotingApiKeyProvider loads and caches API key items with TTL expiry
  • Auth flow checks API Key items first, then falls back to legacy config shared secret
  • Duplicate shared secret values across keys trigger warnings
  • All secret comparisons use timing-safe SecureCompare.FixedTimeEquals

Rate limiting:

  • Per-key throttle enforcement based on Request Limit and Throttle Window
  • Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
  • HTTP 429 returned when limit exceeded

Restriction headers:

  • X-SPE-Restriction -- indicates restrictions are active
  • X-SPE-BlockedCommand -- reports which command was blocked
  • X-SPE-Profile -- reports the active restriction profile

Client support:

  • Invoke-RemoteScript in the remoting module handles 429 (rate limit) and 403 (restriction) responses

Security Hardening

Several cross-cutting security improvements were made:

  • Item path restrictions -- profiles can deny access to sensitive content tree paths, protecting API Keys and security configuration from remote scripts
  • Dynamic invocation rejection -- expressions like & $variable are rejected when a blocklist is active, preventing blocklist bypass
  • Fail-closed defaults -- unknown or misconfigured profile references resolve to DenyAll
  • Timing-safe comparison -- all shared secret comparisons use SecureCompare.FixedTimeEquals to prevent timing attacks
  • SOAP coverage -- restriction profile enforcement applies to RemoteAutomation.asmx (SOAP) in addition to REST endpoints
  • Nested folder support -- all Settings subtrees (Restriction Profiles, Trusted Scripts, API Keys) support nested folders for organization
  • Unknown profile warnings -- ProfileOverrideProvider warns when override items reference non-existent profile names

Template Organization

All new templates are organized under a Remoting parent:

/sitecore/templates/Modules/PowerShell Console/Remoting/
  Restriction Profile
  Trusted Script
  Remoting API Key

Settings items live under:

/sitecore/system/Modules/PowerShell/Settings/Remoting/
  Restriction Profiles/
  Trusted Scripts/
    SPE/          (ships with 16 core trusted scripts)
  API Keys/

Implementation History

Commit Phase Description
1 Core CLM Infrastructure RestrictionProfile model, RestrictionProfileManager, ScriptTrustRegistry, 4 predefined profiles, integration tests (8 groups)
2 Item-Based Profile Overrides Restriction Profile template, ProfileOverrideProvider, cache with TTL, integration tests
3 Zero-Trust Script Allowlist Generate-TrustedScripts.ps1, AST-based export extraction, SHA256 hashing (later superseded by Phase 6 simplification)
4 Item-Based Trust Management Trusted Script template, profile-bound trust, TrustedScriptSaveHandler, cache invalidation
5 Remoting API Keys API Key template, RemotingApiKeyProvider, throttling, rate limit headers, restriction headers, client-side 429/403 handling
6 Simplify Trust Model Binary trust (removed Trust Level), Treelist script references, removed config-based trust entirely, SOAP enforcement, dynamic invocation rejection, fail-closed DenyAll, Enabled checkboxes, template reorganization
7 Item Path Restrictions ItemPathRestrictions on profiles (blocklist/allowlist with prefix matching), provider-level enforcement in PsSitecoreItemProvider, Treelist override fields, default restrictions protecting Remoting settings
8 JWT Warning Suppression SuppressWarnings flag on auth provider during API Key probing, promote successful key match to INFO
9 MCP Integration X-SPE-LanguageMode response header, New-PSObject cmdlet (CLM-safe [PSCustomObject] alternative)
10 Content-Editor Allowlist Add New-PSObject to content-editor profile allowlist
11 Remove Get-SitecoreVersion Redundant -- $PSVersionTable.SitecoreVersion already works under CLM

MCP Server Integration

Issues reported during MCP server integration testing against SPE 9.0 with CLM enabled. SPE's CLM implementation delegates entirely to PowerShell's native ConstrainedLanguage mode -- it sets runspace.SessionStateProxy.LanguageMode = ConstrainedLanguage and adds no custom type restrictions. Most reported issues are standard PS CLM behavior, not SPE bugs.

Issue analysis and resolutions:

Issue Root Cause Resolution
[PSCustomObject]@{} blocked Standard PS CLM behavior. The construction syntax creates arbitrary objects with caller-defined properties, which CLM blocks. The type accelerator is allowed for casting, but not for construction. Added New-PSObject cmdlet -- accepts a hashtable, returns a PSCustomObject. Works under CLM because cmdlet code runs in C#.
Static .NET method calls blocked Standard PS CLM behavior. Property access on any type is allowed, but method calls on non-core types are blocked. Use $PSVersionTable.SitecoreVersion (already available, works under CLM as a variable access).
CliXml leaking in responses The <#messages#> delimiter is SPE's intentional mechanism for separating stdout from error stream in clixml/raw output modes. Not a bug. Use outputFormat=json&errorFormat=structured for clean JSON responses.
Remove-ScriptSession not effective Session keys include the HTTP session ID. Calling Remove-ScriptSession from a different HTTP context won't match. Send a no-op script with persistentSession=false to auto-dispose.
Find-Item -Criteria under CLM The hashtable-to-SearchCriteria coercion may be blocked under CLM because SearchCriteria is not a core type. Under investigation.
Language mode detection Required a separate probe request. Added X-SPE-LanguageMode response header on all remoting responses.

New cmdlets

New-PSObject -- CLM-safe replacement for [PSCustomObject]@{ ... }. Accepts a hashtable, returns a PSCustomObject. Allowed in all profiles (blocklist profiles don't block it; content-editor allowlist includes it).

# Single object
New-PSObject @{ Name = 'Home'; Path = '/sitecore/content/Home'; TemplateId = '{76036F5E-...}' }

# Pipe to JSON
New-PSObject @{ ItemCount = 42; Database = 'master'; Timestamp = (Get-Date).ToString('o') } | ConvertTo-Json

# Pipe array to JSON -- uses parenthesis
$data = @(
    (New-PSObject @{ Name = 'Alpha'; Type = 'Page'; Count = 42 }),
    (New-PSObject @{ Name = 'Beta'; Type = 'Component'; Count = 7 })
)
$data | ConvertTo-Json

# Pipe hashtables to JSON
@{ Name = 'Alpha'; Type = 'Page'; Count = 42 },
@{ Name = 'Beta'; Type = 'Component'; Count = 7 } | 
    ForEach-Object { New-PSObject $_ } | 
    ConvertTo-Json

# Build results from queried items
Get-ChildItem -Path "master:/sitecore/content/Home" |
    ForEach-Object { New-PSObject @{ Name = $_.Name; Path = $_.ItemPath; Template = $_.TemplateName } } |
    ConvertTo-Json

# Multiple objects as an array
@(
    New-PSObject @{ Status = 'OK'; Service = 'remoting' }
    New-PSObject @{ Status = 'OK'; Service = 'restfulv2' }
) | ConvertTo-Json

# Computed result with no source item
New-PSObject @{
    Sitecore    = $PSVersionTable.SitecoreVersion.ToString()
    SPEVersion  = $PSVersionTable.SPEVersion
    Language    = [Sitecore.Context]::Language.Name
    User        = [Sitecore.Context]::User.Name
} | ConvertTo-Json

$PSVersionTable.SitecoreVersion -- already available in every SPE session. Works under CLM because it's a variable access, not a static method call. No new cmdlet needed.

# Sitecore version (System.Version object)
$PSVersionTable.SitecoreVersion

# SPE version
$PSVersionTable.SPEVersion

# Include in a status response
New-PSObject @{
    Sitecore = $PSVersionTable.SitecoreVersion.ToString()
    SPE      = $PSVersionTable.SPEVersion
} | ConvertTo-Json

Response headers

All remoting responses now include:

Header Example Description
X-SPE-LanguageMode ConstrainedLanguage Active language mode for the session. Eliminates the need for a separate probe request.
X-SPE-Restriction command-blocked Set on 403 responses when a command is blocked by the profile.
X-SPE-BlockedCommand Remove-Item The specific command that was blocked.
X-SPE-Profile read-only The restriction profile that blocked the command.

Recommended API consumer configuration:

API consumers should use the JSON output format and API Key authentication:

POST /-/script/script/?sessionId={id}&rawOutput=false&outputFormat=json&errorFormat=structured&persistentSession=false

API Key items (under /Settings/Remoting/API Keys/) provide per-consumer profiles, rate limiting, user impersonation, and eliminate JWT signature mismatch warnings from the legacy shared secret path.


Remaining Work

Documentation and Migration Guide

  • Migration guide for existing installations (config shared secret to API Key items)
  • Admin documentation for creating and managing restriction profiles
  • Admin documentation for trusted script management
  • Admin documentation for item path restrictions
  • CLM scripting guide (allowed patterns, New-PSObject usage, $PSVersionTable variables)
  • README updates for remoting security model

Known Issues

  • SOAP endpoint (RemoteAutomation.asmx) does not enforce restriction profiles (disabled by default, not a security risk)
  • Find-Item -Criteria hashtable coercion under CLM needs investigation

Test Coverage

Integration tests cover 12 groups:

  1. Language mode enforcement (ConstrainedLanguage restrictions apply)
  2. Command blocklist/allowlist enforcement
  3. Module restriction enforcement
  4. Execution escape prevention (dynamic invocation blocking)
  5. Remote session prevention under restricted profiles
  6. JWT scope-to-profile mapping
  7. Backward compatibility (unrestricted profile matches pre-CLM behavior)
  8. Exception survival (errors surface correctly through restriction layer)
  9. Item-based profile overrides (lifecycle with cache expiry)
  10. Item path blocklist (Get-Item, Get-ChildItem, access by ID)
  11. Item path restriction error details (ErrorId, profile name in message)
  12. Get-ChildItem filtering (blocked children excluded from results)

SOAP profile tests and download tests have known failures (see Known Issues).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions