mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-12 20:57:59 +02:00
fix: reject late PowerShell scalar resolution
This commit is contained in:
@@ -1164,6 +1164,13 @@ function createScanState() {
|
||||
|
||||
function collectStaticScalarAssignments(input, state) {
|
||||
const variable = String.raw`(\$\{[^}]+\}|\$(?:[A-Za-z_][\w-]*:)?[A-Za-z_][\w-]*(?:\[[^\]]+\]|\.[A-Za-z_][\w-]*)*)`;
|
||||
const firstReferences = new Map();
|
||||
const referencePattern = new RegExp(variable, 'g');
|
||||
let reference;
|
||||
while ((reference = referencePattern.exec(input)) !== null) {
|
||||
const name = reference[1].toLowerCase();
|
||||
if (!firstReferences.has(name)) firstReferences.set(name, reference.index);
|
||||
}
|
||||
const assignmentCounts = new Map();
|
||||
const assignmentPattern = new RegExp(`${variable}\\s*(?:\\+=|-=|\\*=|\\/=|%=|=)`, 'g');
|
||||
let assignmentMatch;
|
||||
@@ -1177,11 +1184,18 @@ function collectStaticScalarAssignments(input, state) {
|
||||
);
|
||||
let match;
|
||||
while ((match = pattern.exec(input)) !== null) {
|
||||
const name = match[1].toLowerCase();
|
||||
// The scan pre-collects immutable scalars for nested executable bodies.
|
||||
// A value assigned after an earlier reference cannot explain that use.
|
||||
// Keep it unresolved so dynamic execution remains gated. Counting even
|
||||
// quoted references is deliberately conservative, with a linear scan.
|
||||
const assignmentIndex = match.index + match[0].indexOf(match[1]);
|
||||
if (firstReferences.get(name) !== assignmentIndex) continue;
|
||||
if (match[3] !== undefined && /(^|[^`])\$/.test(match[3])) continue;
|
||||
const value = match[2] !== undefined
|
||||
? match[2].replace(/''/g, "'")
|
||||
: decodeDoubleQuotedString(match[3]);
|
||||
state.staticScalars.set(match[1].toLowerCase(), value);
|
||||
state.staticScalars.set(name, value);
|
||||
}
|
||||
for (const [name, count] of assignmentCounts) {
|
||||
if (count !== 1) state.staticScalars.delete(name);
|
||||
|
||||
@@ -2991,7 +2991,8 @@ function runTests() {
|
||||
'cmd /c pwsh -Command "Remove-Item -Force C:/tmp/demo"',
|
||||
'@"\n" # $(Remove-Item -Force C:/tmp/demo)\n"@',
|
||||
'& ‘Remove-Item’ -Force C:/tmp/demo',
|
||||
'Invoke-Expression $runtimeValue'
|
||||
'Invoke-Expression $runtimeValue',
|
||||
'pwsh -Command "$payload"; $payload = "Write-Output ok"'
|
||||
];
|
||||
|
||||
for (const command of commands) {
|
||||
|
||||
@@ -251,6 +251,10 @@ async function runTests() {
|
||||
command: 'pwsh -Command "Write-Output ready; $runtimePayload"',
|
||||
expectedRules: ['powershell.dynamic-execution'],
|
||||
},
|
||||
{
|
||||
command: 'pwsh -Command "$payload"; $payload = "Write-Output ok"',
|
||||
expectedRules: ['powershell.dynamic-execution'],
|
||||
},
|
||||
{
|
||||
command: 'pwsh -Command $runtimePayload -Force C:/private/runtime-command-sentinel',
|
||||
expectedRules: ['powershell.dynamic-execution'],
|
||||
|
||||
@@ -190,6 +190,22 @@ test('classifies pipeline recursion evidence upstream of Remove-Item', () => {
|
||||
|
||||
console.log('\nNested shell payloads:');
|
||||
|
||||
test('does not resolve earlier invocations from later scalar assignments', () => {
|
||||
for (const invocation of [
|
||||
'pwsh -Command "$payload"',
|
||||
'pwsh -Command:$payload',
|
||||
'pwsh -EncodedCommand:$payload',
|
||||
'Invoke-Expression $payload',
|
||||
'& $payload',
|
||||
]) {
|
||||
expectRules(`${invocation}; $payload = 'Write-Output ok'`, [RULES.DYNAMIC_EXECUTION]);
|
||||
}
|
||||
expectRules('pwsh -Command "$payload"; $payload = "Remove-Item -Force C:/tmp/demo"', [
|
||||
RULES.DYNAMIC_EXECUTION,
|
||||
]);
|
||||
expectSafe('$payload = "Write-Output ok"; pwsh -Command "$payload"');
|
||||
});
|
||||
|
||||
test('classifies powershell and pwsh command payloads recursively', () => {
|
||||
expectRules(
|
||||
'powershell -Command "Remove-Item -Recurse C:/tmp/demo"',
|
||||
|
||||
Reference in New Issue
Block a user