mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-18 07:37:59 +02:00
fix: reject late PowerShell scalar resolution
This commit is contained in:
@@ -1164,6 +1164,13 @@ function createScanState() {
|
|||||||
|
|
||||||
function collectStaticScalarAssignments(input, state) {
|
function collectStaticScalarAssignments(input, state) {
|
||||||
const variable = String.raw`(\$\{[^}]+\}|\$(?:[A-Za-z_][\w-]*:)?[A-Za-z_][\w-]*(?:\[[^\]]+\]|\.[A-Za-z_][\w-]*)*)`;
|
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 assignmentCounts = new Map();
|
||||||
const assignmentPattern = new RegExp(`${variable}\\s*(?:\\+=|-=|\\*=|\\/=|%=|=)`, 'g');
|
const assignmentPattern = new RegExp(`${variable}\\s*(?:\\+=|-=|\\*=|\\/=|%=|=)`, 'g');
|
||||||
let assignmentMatch;
|
let assignmentMatch;
|
||||||
@@ -1177,11 +1184,18 @@ function collectStaticScalarAssignments(input, state) {
|
|||||||
);
|
);
|
||||||
let match;
|
let match;
|
||||||
while ((match = pattern.exec(input)) !== null) {
|
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;
|
if (match[3] !== undefined && /(^|[^`])\$/.test(match[3])) continue;
|
||||||
const value = match[2] !== undefined
|
const value = match[2] !== undefined
|
||||||
? match[2].replace(/''/g, "'")
|
? match[2].replace(/''/g, "'")
|
||||||
: decodeDoubleQuotedString(match[3]);
|
: decodeDoubleQuotedString(match[3]);
|
||||||
state.staticScalars.set(match[1].toLowerCase(), value);
|
state.staticScalars.set(name, value);
|
||||||
}
|
}
|
||||||
for (const [name, count] of assignmentCounts) {
|
for (const [name, count] of assignmentCounts) {
|
||||||
if (count !== 1) state.staticScalars.delete(name);
|
if (count !== 1) state.staticScalars.delete(name);
|
||||||
|
|||||||
@@ -2991,7 +2991,8 @@ function runTests() {
|
|||||||
'cmd /c pwsh -Command "Remove-Item -Force C:/tmp/demo"',
|
'cmd /c pwsh -Command "Remove-Item -Force C:/tmp/demo"',
|
||||||
'@"\n" # $(Remove-Item -Force C:/tmp/demo)\n"@',
|
'@"\n" # $(Remove-Item -Force C:/tmp/demo)\n"@',
|
||||||
'& ‘Remove-Item’ -Force C:/tmp/demo',
|
'& ‘Remove-Item’ -Force C:/tmp/demo',
|
||||||
'Invoke-Expression $runtimeValue'
|
'Invoke-Expression $runtimeValue',
|
||||||
|
'pwsh -Command "$payload"; $payload = "Write-Output ok"'
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const command of commands) {
|
for (const command of commands) {
|
||||||
|
|||||||
@@ -251,6 +251,10 @@ async function runTests() {
|
|||||||
command: 'pwsh -Command "Write-Output ready; $runtimePayload"',
|
command: 'pwsh -Command "Write-Output ready; $runtimePayload"',
|
||||||
expectedRules: ['powershell.dynamic-execution'],
|
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',
|
command: 'pwsh -Command $runtimePayload -Force C:/private/runtime-command-sentinel',
|
||||||
expectedRules: ['powershell.dynamic-execution'],
|
expectedRules: ['powershell.dynamic-execution'],
|
||||||
|
|||||||
@@ -190,6 +190,22 @@ test('classifies pipeline recursion evidence upstream of Remove-Item', () => {
|
|||||||
|
|
||||||
console.log('\nNested shell payloads:');
|
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', () => {
|
test('classifies powershell and pwsh command payloads recursively', () => {
|
||||||
expectRules(
|
expectRules(
|
||||||
'powershell -Command "Remove-Item -Recurse C:/tmp/demo"',
|
'powershell -Command "Remove-Item -Recurse C:/tmp/demo"',
|
||||||
|
|||||||
Reference in New Issue
Block a user