Repository navigation
fix(O148): analyze pwsh nested execution deeper; beyond the cap fails closed #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| #!/usr/bin/env node | ||
| /** | ||
| * O148: PowerShell nested execution deeper than one level was silently | ||
| * ignored by the write-target scanner — `& { & { Set-Content <config> x } }` | ||
| * resolved nothing (no hit, no unresolved marker), so a write hidden two or | ||
| * more wrappers deep escaped the gate. The scanner now recurses to a bounded | ||
| * depth and reports anything deeper as unresolved (fail-closed: the caller | ||
| * prompts / refuses instead of silently allowing). | ||
| */ | ||
| import test from 'node:test'; | ||
| import assert from 'node:assert/strict'; | ||
| import fs from 'node:fs'; | ||
| import os from 'node:os'; | ||
| import path from 'node:path'; | ||
|
|
||
| import { findPwshWriteTargets } from '../dist/safety/pwsh-write-targets.js'; | ||
| import { findShellProtectedWrites } from '../dist/safety/protected-paths.js'; | ||
|
|
||
| const root = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-o148-')); | ||
| const profile = path.join(root, 'profile'); | ||
| const cfg = path.join(profile, 'AppData', 'Roaming', 'moss'); | ||
| const ws = path.join(root, 'ws'); | ||
| for (const d of [cfg, ws]) fs.mkdirSync(d, { recursive: true }); | ||
| test.after(() => fs.rmSync(root, { recursive: true, force: true })); | ||
|
|
||
| const CFG = path.join(cfg, 'config.json').replace(/\\/g, '/'); | ||
| const fwd = (s) => s.replace(/\\/g, '/'); | ||
| const env = { USERPROFILE: profile, HOME: profile, APPDATA: path.dirname(cfg), PATH: '/usr/bin' }; | ||
| const ctx = { cwd: ws, workspaceRoot: ws, configDirs: [cfg], env, home: profile }; | ||
| const scan = (cmd) => findPwshWriteTargets(cmd, ctx); | ||
| const hasCfg = (r) => r.resolved.some((p) => fwd(p) === fwd(CFG)); | ||
|
|
||
| test('depth 1 (baseline): & { Set-Content <config> } resolves the target', () => { | ||
| assert.equal(hasCfg(scan(`& { Set-Content '${CFG}' x }`)), true); | ||
| }); | ||
|
|
||
| test('depth 2: & { & { Set-Content <config> } } resolves the target', () => { | ||
| const r = scan(`& { & { Set-Content '${CFG}' x } }`); | ||
| assert.equal(hasCfg(r), true, JSON.stringify(r)); | ||
| }); | ||
|
|
||
| test('depth 3: three wrappers still resolve the target', () => { | ||
| const r = scan(`& { & { & { Set-Content '${CFG}' x } } }`); | ||
| assert.equal(hasCfg(r), true, JSON.stringify(r)); | ||
| }); | ||
|
|
||
| test('depth 2 through iex: iex "& { Set-Content <config> }" resolves', () => { | ||
| const r = scan(`iex "& { Set-Content '${CFG}' x }"`); | ||
| assert.equal(hasCfg(r), true, JSON.stringify(r)); | ||
| }); | ||
|
|
||
| test('deeper than the cap fails closed instead of being ignored', () => { | ||
| const deep = `& { & { & { & { Set-Content '${CFG}' x } } } }`; | ||
| const r = scan(deep); | ||
| assert.ok(r.unresolved.length > 0, `unresolved marker expected; ${JSON.stringify(r)}`); | ||
| // End to end: deep nesting must produce a gate hit — either the resolved | ||
| // target itself or the fail-closed unresolved marker, never silence. | ||
| const hits = findShellProtectedWrites(deep, ctx); | ||
| assert.ok(hits.length > 0, 'deep nesting must produce a protected-path hit'); | ||
| assert.equal( | ||
| hits.some((h) => h.category === 'unresolved' || h.category === 'moss-config'), | ||
| true, | ||
| JSON.stringify(hits) | ||
| ); | ||
| }); | ||
|
|
||
| test('a deeply nested BENIGN body still fails closed (stricter only)', () => { | ||
| const deep = `& { & { & { & { Write-Host hi } } } }`; | ||
| assert.ok(scan(deep).unresolved.length > 0); | ||
| }); | ||
|
|
||
| test('no false hits for ordinary one-level reads', () => { | ||
| const r = scan(`& { Get-Content '${CFG}' }`); | ||
| assert.equal(hasCfg(r), false); | ||
| assert.equal(r.unresolved.length, 0); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For deeply nested scriptblocks,
extractNestedCommands()returns every descendant block, not just immediate children, so each recursive call scans descendants again and the depth cap does not bound the total work or accumulated results. After building, scanning'& { '.repeat(80) + 'Write-Host hi' + ' }'.repeat(80)took about eight seconds and produced 1,581,580 unresolved entries; 100 wrappers (only 613 characters) threwRangeError: Maximum call stack size exceededat the spread intounresolved. SincefindShellProtectedWrites()invokes this scanner synchronously, these inputs stall or abort protected-path checking instead of returning the intended fail-closed marker. Traverse only immediate nested bodies or enforce a shared work/result budget that stops scanning once exhausted.Useful? React with 👍 / 👎.