Skip to content

Constrained Language Mode profiles for SPE remoting security #1426

@michaellwest

Description

@michaellwest

Summary

Predefined Constrained Language Mode (CLM) profiles and configuration for securing SPE remoting endpoints. Based on an audit of all 291 serialized PowerShell scripts across the release/9.0 branch, this proposal catalogs every .NET API, Sitecore library, function, and cmdlet in use, then defines tiered restriction profiles, a script trust model, and a hybrid config/item-based management system.

Scope: Remoting endpoints only (/api/spe/remoting, RESTful v2, custom web API services). ISE, Console, context menus, ribbons, pipelines, event handlers, and scheduled tasks remain unchanged at FullLanguage.


1. API & Cmdlet Catalog

Dangerous Patterns Identified

Pattern Location Risk
Reflection with nonpublic BindingFlags Remoting.yml, Remoting2.yml Bypasses access control
[scriptblock]::Create() Rules Based Report.yml Dynamic code execution
Add-Type -AssemblyName Expand-Archive.yml Assembly loading
Direct SQL via SqlClient Invoke-SqlCommand.yml Database access
[System.Web.Security.Membership] Enforce password expiration.yml Identity manipulation

Not found (good): Invoke-Expression, Start-Process, Add-Type -TypeDefinition, Add-Type -Path

Full catalog of .NET APIs, Sitecore APIs, SPE cmdlets, and standard PowerShell cmdlets available in the design document.


2. Restriction Profiles

Four tiered profiles combining PowerShell's native LanguageMode with SPE's commandRestrictions. All profiles are opt-in -- fully backward-compatible.

Profile: unrestricted (Default)

  • LanguageMode: FullLanguage
  • Command Restrictions: None
  • Use case: Trusted admin scripts, development environments

Profile: read-only-sitecore

  • LanguageMode: ConstrainedLanguage
  • Blocked: Set-Item, Remove-Item, New-Item, Move-Item, Copy-Item, Rename-Item, Publish-Item, Protect-Item, Unprotect-Item, Install-Package, New-Package, Set-Layout, Add-Rendering, Remove-Rendering
  • Allowed .NET Types: Primitives only ([int], [string], [bool], [DateTime], [guid], [regex])
  • Use case: Reporting integrations, dashboards, monitoring

Profile: read-only-full

  • Extends: read-only-sitecore`
  • Additional blocks: Invoke-SqlCommand, Set-Content, Out-File, Export-Csv, Export-Clixml, Add-Type, Compress-Archive, Expand-Archive, Send-SheerMessage
  • Blocked .NET Types: All non-primitive types (via CLM)
  • Blocked Patterns: [System.IO.File]::Write*, [System.Data.SqlClient.*]
  • Use case: Untrusted external consumers, third-party integrations

Profile: content-editor

  • LanguageMode: ConstrainedLanguage
  • Mode: Allowlist
  • Allowed: All read cmdlets from read-only-sitecore + Set-Item (content fields only), Publish-Item, New-Item (content items only), Lock-Item, Unlock-Item
  • Standard fields (__ prefix) blocked by default -- only content fields (defined on the item's own template) are writable. Admins can allowlist specific standard fields via config.
  • Use case: Content management APIs, headless CMS integrations

Profile Inheritance

Single-level only (e.g., read-only-full extends read-only-sitecore). Resolved via load-time flattening -- the ProfileManager merges parent + child blocklists at config load into a flat RestrictionProfile object. No runtime chain-walking.

Add-Type Handling

Block Add-Type entirely in constrained profiles with configurable assembly allowlist:

  • Allowed: System.IO.Compression, System.IO.Compression.FileSystem
  • Always blocked: -TypeDefinition (compiles C#), -Path (loads DLLs), -MemberDefinition (P/Invoke)

SQL Access

Block Invoke-SqlCommand entirely in constrained profiles. If SQL read access is needed, a future Invoke-SqlQuery cmdlet wrapping ExecuteReader only is safer than parsing SQL strings.

File System

  • read-only-sitecore: Allow file reads, block file writes
  • read-only-full: Block all direct file system .NET calls; allow only Get-Content, Test-Path, Resolve-Path, Get-ChildItem

3. Trusted Scripts Model

Problem

SPE scripts use Import-Function, Invoke-Script, and Execute-Script to load Script Library items. These often require .NET type access that CLM blocks. A blanket CLM enforcement would break most SPE built-in functionality.

Solution: GUID + Content Hash Trust Registry

Trust operates on script items (not individual function names) since a single script item (e.g., DialogBuilder) can define multiple functions.

<trustedScripts>
  <script name="Remoting"
          itemId="{E5F6A7B8-...}"
          contentHash="sha256:..."
          trust="System"
          allowTopLevel="true">
    <exports>
      <function>ConvertTo-CliXml</function>
      <function>ConvertFrom-CliXml</function>
    </exports>
  </script>

  <script name="DialogBuilder"
          itemId="{B3E2F1A2-...}"
          contentHash="sha256:..."
          trust="Trusted">
    <exports>
      <function>New-DialogBuilder</function>
      <function>Add-DialogField</function>
      <function>Show-Dialog</function>
    </exports>
  </script>
</trustedScripts>

Trust Levels

  1. Untrusted (default) -- runs under caller's language mode and command restrictions
  2. Trusted -- can use .NET types and CLM bypass; no reflection access
  3. System -- reflection + private member access allowed; config-only assignment

Why GUIDs

  • Sitecore item GUIDs are assigned at creation and never change (survive renames, moves, serialization)
  • SPE ships Script Library items via serialization with fixed GUIDs
  • Cannot create a new item with the same GUID -- Sitecore enforces uniqueness
  • Prevents shadowing: a user-created script with the same name but different GUID will not get trust elevation

Content Hash Integrity

SHA256 of the script body stored alongside the trust entry. Catches tampering or accidental modification.

<script name="..."
        itemId="{...}"
        contentHash="sha256:abc123..."
        trust="Trusted"
        onHashMismatch="constrain" />  <!-- or "block" or "warn" -->

Execution Entry Points

Trust applies at the common execution layer, not per-cmdlet:

Method Trust Check Export Manifest
Import-Function GUID + hash Yes -- controls which functions get elevated status
Invoke-Script GUID + hash No -- return values subject to caller's type restrictions
Execute-Script (internal) GUID + hash (remoting only) Context-dependent

Export Manifest (Import-Function only)

When a trusted script is loaded via Import-Function:

  1. Script body executes in FullLanguage (defines functions)
  2. Functions actually defined are compared against declared <exports>
  3. Undeclared functions are either removed (strict) or forced constrained (permissive)
  4. Prevents a compromised script from injecting elevated functions that shadow built-ins

Resolution Order

In constrained sessions, Import-Function always prefers the GUID-registered script over a name match, preventing shadowing.

Auto-Generation

A build task parses serialized YAML files to auto-generate <trustedScripts> with correct GUIDs, content hashes, and export lists (via AST parsing of function definitions).


4. Scope Claim Integration

JWT scope claims map to profile names:

  • scope=read-only-sitecore applies that profile
  • scope=content-editor applies that profile
  • No scope or unknown scope falls back to service default
  • Multiple scopes: most restrictive wins (intersection)

Builds on the existing scopeRestrictions system in ScriptValidator.cs.


5. Audit Logging

Configurable per profile:

Level What's Logged
None No logging
Violations Blocked commands, denied .NET types, failed trust checks
Standard Violations + script execution start/end with user context
Full Standard + every command executed with arguments

Pipeline scripts (LoggedIn, LoggingIn, Logout) always run in FullLanguage and are logged with [INFO] Pipeline script executing in FullLanguage (exempt from profile restrictions).


6. Hybrid Configuration Model

Base Configuration (Spe.config)

Profiles defined as XML patches. Ship with SPE, provide secure defaults. All services default to unrestricted.

Item-Based Overrides (Phase 2)

Admins extend/override via Sitecore items under /sitecore/system/Modules/PowerShell/Settings/Restriction Profiles/. Merge behavior: most restrictive wins.


7. Migration & Backward Compatibility

All new functionality is opt-in. Existing installations see no behavior change.

Phased Rollout

  1. Phase 1: RestrictionProfile class, config loading with load-time flattening, service-to-profile mapping, command blocklist/allowlist enforcement, GUID-based script trust registry, violations-level audit logging, standard field blocking for content-editor, JWT scope-to-profile mapping
  2. Phase 2: Item-based overrides, Restriction Profile Override template, enhanced trust management UI
  3. Phase 3: AST-based function classification (ReadOnly/WriteCapable/Elevated), content hash auto-generation in build
  4. Phase 4: Documentation and migration guide

Adoption

One attribute change per service:

<remoting enabled="true" requireSecureConnection="true" profile="read-only-sitecore" />

8. New Components

Component Purpose
RestrictionProfile Data model: name + language mode + command restrictions + trust config + audit level
ProfileManager Config loading, inheritance flattening, caching
TrustedScriptEntry GUID + content hash + trust level + export manifest
ScriptTrustRegistry Trust lookup by item GUID at execution time
Extended ScriptValidator Enforce profile restrictions during script execution
Extended WebServiceSettings Map services to profiles

Performance

  • Profile resolution cached per service+scope combination
  • AST analysis cached per script revision
  • Trust lookups by GUID (O(1) dictionary)
  • All caches respect existing Spe.AuthorizationCacheExpirationSecs and Spe.WebApiCacheExpirationSecs

Design Decisions

Decision Choice Rationale
Profile inheritance depth Single level only Avoids merge-order ambiguity
Inheritance resolution Load-time flattening Simpler, faster than runtime chain-walking
Trust identity Item GUID + content hash GUIDs are immutable and unique; hash catches tampering
SQL in constrained profiles Block entirely Safer than parsing SQL strings; future Invoke-SqlQuery cmdlet for read-only access
Temporary remoting elevation Not supported Use separate endpoints with different profiles instead
AST analysis timing Sync on first request, cached Pre-analysis on save misses scripts modified outside Sitecore
Composable scopes Intersection only Union would defeat the purpose of restrictions
Standard fields in content-editor Blocked by default (__ prefix) Prevents security/workflow/rendering tampering; allowlist for exceptions

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