88 lines
3.4 KiB
TypeScript
88 lines
3.4 KiB
TypeScript
/**
|
|
* Regression: runElevatedPowerShell() must no longer use -EncodedCommand and
|
|
* must write the elevated payload to a per-call temp .ps1 file referenced via
|
|
* -File. See docs/security/SOCKET_DEV_FINDINGS.md §1.
|
|
*/
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import os from "node:os";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import {
|
|
buildElevatedScriptWrapper,
|
|
_runElevatedPowerShellForTest,
|
|
} from "../../../src/mitm/systemCommands.ts";
|
|
|
|
test("buildElevatedScriptWrapper does not contain -EncodedCommand fingerprint", () => {
|
|
const wrapper = buildElevatedScriptWrapper("C:\\Temp\\omniroute-elevate-x.ps1");
|
|
assert.ok(
|
|
!wrapper.includes("-EncodedCommand"),
|
|
"wrapper must not contain -EncodedCommand (Socket.dev textbook fingerprint)"
|
|
);
|
|
assert.ok(wrapper.includes("-File"), "wrapper must reference the payload via -File");
|
|
assert.ok(wrapper.includes("Start-Process"), "wrapper must use Start-Process -Verb RunAs");
|
|
assert.ok(wrapper.includes("-Verb RunAs"), "wrapper must request elevation via -Verb RunAs");
|
|
});
|
|
|
|
test("buildElevatedScriptWrapper quotes the script path safely (no shell injection)", () => {
|
|
const wrapper = buildElevatedScriptWrapper("C:\\Temp\\path with spaces'and-quote.ps1");
|
|
// PowerShell single-quote escaping doubles the quote — our quotePowerShell
|
|
// helper does that. Confirm both the original and the escaped form are present.
|
|
assert.ok(
|
|
wrapper.includes("'C:\\Temp\\path with spaces''and-quote.ps1'"),
|
|
"single quotes in the path must be doubled per PowerShell escaping rules"
|
|
);
|
|
});
|
|
|
|
test("_runElevatedPowerShellForTest writes payload to a temp .ps1 file and unlinks after", async () => {
|
|
let capturedWrapper: string | null = null;
|
|
let capturedTempPath: string | null = null;
|
|
|
|
await _runElevatedPowerShellForTest(
|
|
"Write-Output 'omniroute regression test'",
|
|
async (wrapper, tempPath) => {
|
|
capturedWrapper = wrapper;
|
|
capturedTempPath = tempPath;
|
|
assert.ok(fs.existsSync(tempPath), "temp file must exist while the runner is active");
|
|
const content = fs.readFileSync(tempPath, "utf8");
|
|
assert.match(content, /Write-Output 'omniroute regression test'/);
|
|
return "ok";
|
|
}
|
|
);
|
|
|
|
assert.ok(capturedWrapper, "wrapper must be captured");
|
|
assert.ok(!capturedWrapper!.includes("-EncodedCommand"), "wrapper must not use -EncodedCommand");
|
|
assert.ok(capturedTempPath, "temp path must be captured");
|
|
assert.ok(
|
|
capturedTempPath!.startsWith(path.resolve(os.tmpdir())) ||
|
|
capturedTempPath!.startsWith(os.tmpdir()),
|
|
"temp .ps1 must live inside os.tmpdir()"
|
|
);
|
|
assert.ok(
|
|
!fs.existsSync(capturedTempPath!),
|
|
"temp .ps1 file must be unlinked after the runner returns"
|
|
);
|
|
});
|
|
|
|
test("_runElevatedPowerShellForTest unlinks the temp file even when the runner throws", async () => {
|
|
let capturedTempPath: string | null = null;
|
|
let threw = false;
|
|
|
|
try {
|
|
await _runElevatedPowerShellForTest("Write-Output 'denied'", async (_wrapper, tempPath) => {
|
|
capturedTempPath = tempPath;
|
|
throw new Error("simulated UAC denial");
|
|
});
|
|
} catch (err) {
|
|
threw = true;
|
|
assert.match((err as Error).message, /simulated UAC denial/);
|
|
}
|
|
|
|
assert.ok(threw, "the error must propagate to the caller");
|
|
assert.ok(capturedTempPath, "temp path must still be captured");
|
|
assert.ok(
|
|
!fs.existsSync(capturedTempPath!),
|
|
"temp .ps1 must be removed by the finally block even after a failed call"
|
|
);
|
|
});
|