mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-18 21:00:22 -05:00
fix(remote): isolate Windows PowerShell module search paths
This commit is contained in:
1 parent
99d9851173
commit
b265454b93
4 files changed
+45
-7
No files matched your search
@@ -66,7 +66,7 @@
|
||||
"src/policy.js": "baad5eb8c3eb2e416e0a71b342ad850d7c8413d7d976571ebf596937f69da8e2",
|
||||
"src/process-runner.js": "49533b038044dfb8bc76ed01c030d06a9856ead0836157fb693e2a7d40f786d6",
|
||||
"src/redact.js": "ebf1afff46341078706b0401838c53db043603586e280d51ece5cf1feba35189",
|
||||
"src/remote.js": "68f901411063fd515a52dad41ffd5c599765a53de34a080715232825eaf207b3",
|
||||
"src/remote.js": "ea433cf719729963a50ebede9442136e72f1eff145c0b3c5e837c6c56fa1ccdf",
|
||||
"src/repo-trust.js": "06e2a94d7113ed936f208a12b7fcc785801c215a3e2c5e7418f6238d991a289c",
|
||||
"src/sdk.d.ts": "d4c80689248c197b2ddb1ebf4cd7d8f2e55716722074d9e1cdb8029b2dfa19f7",
|
||||
"src/sdk.js": "1ac29fb389ed86e0f62ecfddbbd31614c58e832a8181495caa83fe5cf8b30bc4",
|
||||
@@ -75,7 +75,7 @@
|
||||
"src/training.js": "bd8a6c12eba4f9d2f8990fa9692cd04c743ea247f9b13e10f8717ceefcbd09ac",
|
||||
"src/ui/console-widget.js": "0d2f707d57b5ec54968dee1dc80f31c7db9152dcadfc48700c18379dabb55462"
|
||||
},
|
||||
"filesDigest": "f609508982d1fdc67fa7b90e1286e25945f920ba45e8fde95f76234848cf3be9",
|
||||
"filesDigest": "edc6be28115ad21f18954066e989a6b4aaae2137b2f4e6d6571312beef610288",
|
||||
"brainDigest": "c0fb7b079ded157059b91601361429944697dae3cc42abc00dfe1a680986b0f4",
|
||||
"gateFingerprint": "6e53c784eee38310188948fc75fb49e6b4ebc04e247d01b903fa8c8a92d67bdd",
|
||||
"developmentPins": {
|
||||
|
||||
@@ -1 +1 @@
|
||||
4c69daba76af8460fe5101da07aeab58cd84376ca642280e038f52cf3c4676d6 manifest.json
|
||||
0517e22827b8acc7d8550e1e560e4f13ca79397977c52c30cf23770dead2327a manifest.json
|
||||
@@ -60,10 +60,14 @@ const OWNER_ONLY_ACL = [
|
||||
* inherited ACLs. Replace them with a single full-control grant for the owner.
|
||||
* Fails closed: an unrestricted hosts file is an error, not a warning.
|
||||
*/
|
||||
export function restrictToOwner(path, { platform = process.platform, exec = spawnSync } = {}) {
|
||||
export function restrictToOwner(path, { platform = process.platform, exec = spawnSync, env = process.env } = {}) {
|
||||
if (platform !== 'win32') return 'posix-0600';
|
||||
// pwsh -> node -> powershell.exe otherwise inherits PS7's incompatible
|
||||
// module paths. Let Windows PowerShell construct its own defaults without
|
||||
// changing the caller's environment or execution policy (about_PSModulePath).
|
||||
const childEnv = Object.fromEntries(Object.entries(env).filter(([key]) => key.toUpperCase() !== 'PSMODULEPATH'));
|
||||
const r = exec('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', OWNER_ONLY_ACL], {
|
||||
input: JSON.stringify({ path }), encoding: 'utf8', windowsHide: true, timeout: 10_000, maxBuffer: 16_384,
|
||||
input: JSON.stringify({ path }), env: childEnv, encoding: 'utf8', windowsHide: true, timeout: 10_000, maxBuffer: 16_384,
|
||||
});
|
||||
if (r.error || r.status !== 0) {
|
||||
throw new Error(`could not restrict ${path} to the current user (ACL helper exit ${r.status ?? r.error?.code}); the hosts file was not made private`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { tmpdir, userInfo } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
@@ -131,15 +131,49 @@ test('Windows hosts-file restriction removes explicit grants and handles literal
|
||||
});
|
||||
});
|
||||
|
||||
test('Windows ACL helper isolates incompatible inherited PowerShell modules', { skip: process.platform !== 'win32' }, async () => {
|
||||
await withHosts(async (path) => {
|
||||
const modules = join(dirname(path), 'modules');
|
||||
const utility = join(modules, 'Microsoft.PowerShell.Utility');
|
||||
mkdirSync(utility, { recursive: true });
|
||||
// Reproduce pwsh7 -> node -> Windows PowerShell5 without installing pwsh:
|
||||
// this higher-priority module advertises the cmdlet but requires PS7.
|
||||
writeFileSync(join(utility, 'Microsoft.PowerShell.Utility.psd1'),
|
||||
"@{ ModuleVersion='7.0.0'; PowerShellVersion='7.0'; CmdletsToExport=@('ConvertFrom-Json') }");
|
||||
const inherited = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.toUpperCase() !== 'PSMODULEPATH'));
|
||||
inherited.PSModulePath = `${modules};${process.env.PSModulePath || ''}`;
|
||||
const original = { ...inherited };
|
||||
const parentPath = process.env.PSModulePath;
|
||||
const control = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', "'{}' | ConvertFrom-Json"], {
|
||||
env: inherited, encoding: 'utf8', windowsHide: true, timeout: 10_000,
|
||||
});
|
||||
assert.equal(control.status, 1, control.stderr);
|
||||
assert.match(control.stderr, /CouldNotAutoloadMatchingModule/);
|
||||
const saved = saveHost({ name: 'pi', ssh: 'ruv@pi.local' }, path, { env: inherited });
|
||||
assert.equal(saved.access, 'windows-acl-owner-only');
|
||||
assert.ok(Object.keys(inherited).length === Object.keys(original).length
|
||||
&& Object.entries(original).every(([key, value]) => inherited[key] === value),
|
||||
'the caller environment is unchanged');
|
||||
assert.equal(process.env.PSModulePath, parentPath, 'the process environment is unchanged');
|
||||
const acl = spawnSync('icacls', [path], { encoding: 'utf8' });
|
||||
assert.equal(acl.status, 0, acl.stderr);
|
||||
const grants = acl.stdout.split('\n').filter(line => /:\(/.test(line));
|
||||
assert.equal(grants.length, 1, acl.stdout);
|
||||
});
|
||||
});
|
||||
|
||||
test('Windows hosts-file restriction fails closed and keeps input out of PowerShell source', () => {
|
||||
const calls = [];
|
||||
const path = "C:/x/hosts $(throw 'injected').json";
|
||||
const ok = restrictToOwner(path, { platform: 'win32', exec: (cmd, args, options) => { calls.push({ cmd, args, options }); return { status: 0 }; } });
|
||||
const env = { SystemRoot: 'C:/Windows', pSmOdUlEpAtH: 'incompatible-PS7-modules', KEEP: 'unchanged' };
|
||||
const ok = restrictToOwner(path, { platform: 'win32', env, exec: (cmd, args, options) => { calls.push({ cmd, args, options }); return { status: 0 }; } });
|
||||
assert.equal(ok, 'windows-acl-owner-only');
|
||||
assert.equal(calls[0].cmd, 'powershell.exe');
|
||||
assert.deepEqual(calls[0].args.slice(0, 4), ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command']);
|
||||
assert.ok(!calls[0].args.at(-1).includes(path));
|
||||
assert.deepEqual(JSON.parse(calls[0].options.input), { path });
|
||||
assert.deepEqual(calls[0].options.env, { SystemRoot: 'C:/Windows', KEEP: 'unchanged' });
|
||||
assert.equal(env.pSmOdUlEpAtH, 'incompatible-PS7-modules');
|
||||
for (const result of [{ status: 5 }, { status: null, error: { code: 'ENOENT' } }, { status: null, error: { code: 'ETIMEDOUT' } }]) {
|
||||
assert.throws(() => restrictToOwner(path, { platform: 'win32', exec: () => result }), /not made private/);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user