Skip to content

[codex] Trust protected PowerShell parsers and reject untrusted wrappers#30628

Draft
bookholt-oai wants to merge 11 commits into
mainfrom
bookholt/psec-4922-trusted-pwsh-parser
Draft

[codex] Trust protected PowerShell parsers and reject untrusted wrappers#30628
bookholt-oai wants to merge 11 commits into
mainfrom
bookholt/psec-4922-trusted-pwsh-parser

Conversation

@bookholt-oai

@bookholt-oai bookholt-oai commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Why

Codex inspects PowerShell commands before deciding whether they may run. On Windows, that inspection previously launched the PowerShell executable named by the command, so a model- or repository-selected powershell.exe or pwsh.exe could execute before the normal command-policy, approval, and sandbox boundary.

What

  • Select Windows PowerShell and PowerShell 7 parsers only from protected Windows Known Folder locations.
  • Isolate the protected parser from repository-controlled startup hooks, module paths, profiles, working directories, and command resolution.
  • Keep parser trust separate from the runtime wrapper requested for execution; policy inspection never launches the model-selected wrapper.
  • Fail closed for untrusted runtime wrappers and protected-parser failures. Trusted, parsed commands retain their existing inner-command policy behavior; trusted but unsupported commands keep the existing generic outer-wrapper policy path on their exact argv.
  • Preserve the exact PowerShell argv as the approval/cache and outside-sandbox policy identity, including runtime spelling, flavor, flags, script, and Windows path aliases.
  • Cover bare, relative, workspace, case/separator, reparse, namespace, missing-installation, environment-poisoning, and parse-time assembly-resolution variants.

How

Parser resolution derives the Windows PowerShell path from the System Known Folder and the PowerShell 7 path from the machine-wide Program Files Known Folder. It canonicalizes both the protected root and candidate, rejects reparse escapes and non-files, and launches only that verified parser.

The parser child runs from its canonical protected installation directory with a cleared, minimal environment. A module-free, length-bounded framed protocol uses only .NET primitives, so request handling does not invoke autoloadable PowerShell cmdlets or inherit runtime/profiler hooks, user module paths, profiles, or repository-relative discovery.

Before any script is sent to PowerShell, a deliberately conservative raw-source gate rejects parse-time module and DSC constructs, escaped forms, and assembly-qualified type syntax. The assembly gate becomes sticky after a raw [ and rejects any later raw comma, covering nested types and block-comment tricks before ParseInput can resolve a rooted local DLL or UNC assembly identity. Benign false positives are treated as opaque rather than sent to the parser.

PowerShell classification records runtime trust independently from parse success. A trusted, parsed runtime continues through normal inner-command policy evaluation. A trusted but unsupported command follows the existing generic outer-wrapper path on its unchanged full argv: it remains non-known-safe, so restricted modes sandbox or prompt unless an explicit outer-command policy allows it. A protected-parser process or protocol failure is terminal, and an untrusted runtime is always forbidden whether or not its body parses. This preserves protected-runtime compatibility without letting a hidden inner command inherit a safelist result.

For parsed scripts, lowered inner commands can make the result stricter and can permit sandboxed execution, but they cannot authorize sandbox bypass: the eventual PowerShell runtime can resolve profiles, modules, and external commands differently from the isolated parser. Only an exact full-argv outer Allow can override heuristic concerns and run outside the sandbox. Explicit inner or outer Prompt/Forbidden rules remain strictest, and every proposed persistent amendment binds the unchanged outer argv.

This intentionally keeps parser provenance, untrusted-wrapper fail-closed behavior, conservative pre-parse gating, and exact approval identity in one review boundary. The larger-than-usual diff is primarily the platform and policy regression matrix needed to cover that boundary.

This PR is a stack-only security prerequisite, not a standalone product fix. A non-Known-Folder custom or PATH-selected PowerShell remains terminal here. The downstream composed one-shot policy layer must restore eligible custom-runtime execution without classifier-time launch, cache reuse, or durable PATH-sensitive authority before this stack lands.

Testing

  • just test -p codex-shell-command (149 tests passed)
  • Focused codex-core PowerShell policy tests (99 tests passed)
  • Focused command-canonicalization tests (5 tests passed)
  • just fix and just fmt
  • Native Windows CI is required for Known Folder resolution, WinPS and pwsh protected-parser execution, hostile environment isolation, rooted local/UNC assembly no-touch checks, and Windows-only path variants.

Related: PSEC-4922

@bookholt-oai bookholt-oai changed the title [codex] Trust only system PowerShell parsers on Windows [codex] Trust protected PowerShell parsers and reject opaque wrappers Jul 3, 2026
@bookholt-oai bookholt-oai changed the title [codex] Trust protected PowerShell parsers and reject opaque wrappers [codex] Trust protected PowerShell parsers and reject untrusted wrappers Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant