Skip to content

Commit 14b40fd

Browse files
committed
fix(windows): pin the Node runtime so validators cannot be redirected
Get-DreamSkinNodeRuntime accepted $env:CODEX_DREAM_SKIN_NODE and, failing that, whatever node.exe PATH resolved to. Get-DreamSkinValidatedNodeRuntime then 'validated' the candidate by running `node -p process.versions.node` -- so a hostile binary executed before any check ran. Validation was execution. That runtime runs every validator this project owns: Safe CSS (theme-windows.ps1:293), the theme package validator (:1038), image metadata limits (:246) and the injector (:1269). Anyone able to write HKCU\Environment -- no admin rights required -- could point all four at their own node.exe and bypass them at once, including the Safe CSS allowlist that community themes are gated on. macOS never had this: require_signed_node_runtime pins the path inside the ChatGPT bundle, codesigns it and compares team IDs, with no env override. This closes the asymmetry. - Pin to the engine's bundled runtime\node\node.exe, which the installer stages and the engine manifest hash-verifies. No env override, no PATH. - Add Assert-DreamSkinTrustedNodeImage, called before the first execution: Authenticode status must be Valid and the signer must be OpenJS/Node.js Foundation or Microsoft. - run-tests.ps1 asserted the env var was present, pinning the unsafe shape in place -- same trap as the -32000 readiness test. It now asserts the opposite, plus that no PATH fallback exists and that the signature check precedes the probe. Verified by injection: each regression flips its assertion red.
1 parent 3aaaf7d commit 14b40fd

2 files changed

Lines changed: 49 additions & 15 deletions

File tree

windows/scripts/common-windows.ps1

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,22 @@ function Invoke-DreamSkinNative {
419419
}
420420
}
421421

422+
function Assert-DreamSkinTrustedNodeImage {
423+
param([Parameter(Mandatory = $true)][string]$Path)
424+
425+
# Runs BEFORE the binary is ever executed. Get-DreamSkinValidatedNodeRuntime
426+
# learns the version by running `node -p`, so any authenticity check placed
427+
# after that point would already have executed attacker-controlled code.
428+
$signature = Get-AuthenticodeSignature -LiteralPath $Path -ErrorAction Stop
429+
if ("$($signature.Status)" -ine 'Valid') {
430+
throw "The Node.js runtime is not validly signed: $Path ($($signature.Status))."
431+
}
432+
$subject = "$($signature.SignerCertificate.Subject)"
433+
if ($subject -notmatch '(?i)O=(OpenJS Foundation|Node\.js Foundation|Microsoft Corporation)') {
434+
throw "The Node.js runtime is signed by an unexpected publisher: $subject"
435+
}
436+
}
437+
422438
function Get-DreamSkinValidatedNodeRuntime {
423439
param(
424440
[Parameter(Mandatory = $true)][string]$Path,
@@ -428,6 +444,7 @@ function Get-DreamSkinValidatedNodeRuntime {
428444
if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) {
429445
throw "Node.js runtime does not exist: $candidate"
430446
}
447+
Assert-DreamSkinTrustedNodeImage -Path $candidate
431448
$versionProbe = Invoke-DreamSkinNative -FilePath $candidate -ArgumentList @('-p', 'process.versions.node') -DiscardStderr
432449
$version = ($versionProbe.Output -join '').Trim()
433450
if ($versionProbe.ExitCode -ne 0 -or -not $version) { throw 'The Node.js runtime could not be validated.' }
@@ -446,22 +463,20 @@ function Get-DreamSkinValidatedNodeRuntime {
446463
function Get-DreamSkinNodeRuntime {
447464
param([int]$MinimumMajor = 22)
448465

449-
if ($env:CODEX_DREAM_SKIN_NODE) {
450-
return Get-DreamSkinValidatedNodeRuntime -Path $env:CODEX_DREAM_SKIN_NODE -MinimumMajor $MinimumMajor
451-
}
452-
466+
# The runtime that runs Safe CSS validation, theme-package validation, image
467+
# metadata limits and the injector is pinned to the engine's own bundled
468+
# copy, which install-dream-skin.ps1 stages and the engine manifest
469+
# hash-verifies. Neither an environment variable nor PATH may redirect it:
470+
# anyone able to write HKCU\Environment (no admin needed) could otherwise
471+
# point every validator at their own node.exe and bypass all of them at once.
472+
# macOS pins the same way -- see require_signed_node_runtime in
473+
# macos/scripts/common-macos.sh.
453474
$runtimeRoot = Split-Path -Parent $PSScriptRoot
454475
$bundledNode = Join-Path $runtimeRoot 'runtime\node\node.exe'
455-
if (Test-Path -LiteralPath $bundledNode -PathType Leaf) {
456-
return Get-DreamSkinValidatedNodeRuntime -Path $bundledNode -MinimumMajor $MinimumMajor
457-
}
458-
459-
$command = Get-Command node.exe -ErrorAction SilentlyContinue
460-
if (-not $command) { $command = Get-Command node -ErrorAction SilentlyContinue }
461-
if (-not $command) {
462-
throw "Bundled Node.js is missing and Node.js $MinimumMajor or newer was not found in PATH."
476+
if (-not (Test-Path -LiteralPath $bundledNode -PathType Leaf)) {
477+
throw "The bundled Node.js runtime is missing: $bundledNode. Reinstall Codex Dream Skin to restore it."
463478
}
464-
return Get-DreamSkinValidatedNodeRuntime -Path $command.Source -MinimumMajor $MinimumMajor
479+
return Get-DreamSkinValidatedNodeRuntime -Path $bundledNode -MinimumMajor $MinimumMajor
465480
}
466481

467482
function ConvertTo-DreamSkinCodexInstall {

windows/tests/run-tests.ps1

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,16 +175,35 @@ try {
175175
throw 'Runtime scripts are not unblocked only after staged byte-content verification.'
176176
}
177177
foreach ($requiredNodeBehavior in @(
178-
'$env:CODEX_DREAM_SKIN_NODE',
179178
'runtime\node\node.exe',
180179
'runtime\node\LICENSE',
181180
'$sourceHasBundledRuntime',
182-
'Get-DreamSkinValidatedNodeRuntime'
181+
'Get-DreamSkinValidatedNodeRuntime',
182+
'Assert-DreamSkinTrustedNodeImage'
183183
)) {
184184
if (-not $commonSource.Contains($requiredNodeBehavior)) {
185185
throw "Bundled Node.js discovery is missing: $requiredNodeBehavior"
186186
}
187187
}
188+
# The Node runtime executes every validator we own (Safe CSS, theme package,
189+
# image metadata, injector), so its path must not be redirectable by anyone
190+
# who can write HKCU\Environment without admin rights.
191+
if ($commonSource.Contains('$env:CODEX_DREAM_SKIN_NODE')) {
192+
throw 'The Node.js runtime path must not be overridable through an environment variable.'
193+
}
194+
if ($commonSource -match "Get-Command node(\.exe)? -ErrorAction SilentlyContinue") {
195+
throw 'The Node.js runtime must not fall back to whatever node.exe PATH resolves to.'
196+
}
197+
# Authenticity must be proven before the binary runs; `node -p` is execution.
198+
$trustIndex = $commonSource.IndexOf(
199+
'Assert-DreamSkinTrustedNodeImage -Path $candidate', [System.StringComparison]::Ordinal
200+
)
201+
$probeIndex = $commonSource.IndexOf(
202+
"Invoke-DreamSkinNative -FilePath `$candidate", [System.StringComparison]::Ordinal
203+
)
204+
if ($trustIndex -lt 0 -or $probeIndex -le $trustIndex) {
205+
throw 'The Node.js runtime is executed before its signature is verified.'
206+
}
188207
$trayGuardIndex = $installSource.IndexOf('if (Test-DreamSkinTrayActive)', [System.StringComparison]::Ordinal)
189208
$engineInstallIndex = $installSource.IndexOf('$engine = Install-DreamSkinRuntimeEngine', [System.StringComparison]::Ordinal)
190209
if ($trayGuardIndex -lt 0 -or $engineInstallIndex -le $trayGuardIndex) {

0 commit comments

Comments
 (0)