circle-ir 4.12.0 → 4.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/dist/analysis/constant-propagation/propagator.d.ts +8 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +96 -3
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +4 -0
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +168 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.js +657 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -0
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +90 -0
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +51 -54
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +328 -51
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/browser/circle-ir.js +922 -86
- package/dist/core/circle-ir-core.cjs +89 -3
- package/dist/core/circle-ir-core.js +89 -3
- package/dist/core/extractors/dfg.js +17 -0
- package/dist/core/extractors/dfg.js.map +1 -1
- package/package.json +1 -1
|
@@ -40,7 +40,7 @@ import { sanitizerCoversSink } from '../sanitizer-index.js';
|
|
|
40
40
|
* code, resolved constant) are unaffected.
|
|
41
41
|
*/
|
|
42
42
|
const CONTAINER_READ_SOURCES = new Set(['plugin_param', 'config_param']);
|
|
43
|
-
function makeConstPropVeto(constProp, graph, sources, code) {
|
|
43
|
+
function makeConstPropVeto(constProp, graph, sources, code, language) {
|
|
44
44
|
const lines = typeof code === 'string' ? code.split('\n') : [];
|
|
45
45
|
const selfReassign = new Map();
|
|
46
46
|
const escape = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -78,6 +78,13 @@ function makeConstPropVeto(constProp, graph, sources, code) {
|
|
|
78
78
|
return false;
|
|
79
79
|
if (r.reason !== 'variable_not_tainted')
|
|
80
80
|
return true;
|
|
81
|
+
// cognium-dev#579: const-prop has no C# source model, so its tainted set is
|
|
82
|
+
// empty for C# and "not tainted" there means "never looked", not "clean".
|
|
83
|
+
// It still records a plain reassignment (`s = q;`) as a tracked symbol,
|
|
84
|
+
// which vetoed every C# flow through one while `var s = q;` passed. Dead
|
|
85
|
+
// code and resolved constants (the reasons above) still apply.
|
|
86
|
+
if (language === 'csharp')
|
|
87
|
+
return false;
|
|
81
88
|
const lo = Math.min(sourceLine, sinkLine);
|
|
82
89
|
// Walk the reaching-def chain back from this step, stepping through
|
|
83
90
|
// self-derived reassignments only. Reaching defs are per method, so a
|
|
@@ -130,7 +137,7 @@ export class TaintPropagationPass {
|
|
|
130
137
|
const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
|
|
131
138
|
// cognium-dev#328 — scoped exemption from const-prop's `variable_not_tainted`
|
|
132
139
|
// veto for self-derived reassignment (`x = f(x)`, `x += y`), the #287 shape.
|
|
133
|
-
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
|
|
140
|
+
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code, ctx.language);
|
|
134
141
|
// Filter flows: eliminate dead-code paths and constant-propagation FPs
|
|
135
142
|
const verifiedFlows = propagationResult.flows.filter(flow => {
|
|
136
143
|
if (constProp.unreachableLines.has(flow.sink.line))
|
|
@@ -1027,7 +1034,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
1027
1034
|
// detectExpressionScanFlows. Suppress when the variable is
|
|
1028
1035
|
// demonstrably rewritten to a literal between source and sink.
|
|
1029
1036
|
if (typeof code === 'string' &&
|
|
1030
|
-
isReassignedToLiteralBetween(code, varName, source.line, sink.line)) {
|
|
1037
|
+
isReassignedToLiteralBetween(code, varName, source.line, sink.line, unreachableLines)) {
|
|
1031
1038
|
continue;
|
|
1032
1039
|
}
|
|
1033
1040
|
flows.push({
|
|
@@ -1076,7 +1083,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
1076
1083
|
continue;
|
|
1077
1084
|
}
|
|
1078
1085
|
if (typeof code === 'string' &&
|
|
1079
|
-
isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line)) {
|
|
1086
|
+
isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line, unreachableLines)) {
|
|
1080
1087
|
continue;
|
|
1081
1088
|
}
|
|
1082
1089
|
flows.push({
|
|
@@ -1199,7 +1206,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
1199
1206
|
const exists = flows.some(f => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
|
|
1200
1207
|
if (!exists) {
|
|
1201
1208
|
if (typeof code === 'string' &&
|
|
1202
|
-
isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
|
|
1209
|
+
isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line, unreachableLines)) {
|
|
1203
1210
|
continue;
|
|
1204
1211
|
}
|
|
1205
1212
|
// Note: DFG-flow filter handles sanitizer-guard suppression
|
|
@@ -1290,6 +1297,117 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
1290
1297
|
// whole file on every call — O(lines) per call, O(n²) across a large file
|
|
1291
1298
|
// (cognium-ai#305). Within a single analysis all calls pass the same `code`
|
|
1292
1299
|
// reference, so a 1-slot cache collapses the repeated splits to one.
|
|
1300
|
+
/**
|
|
1301
|
+
* C# alias expansion that respects method boundaries (cognium-dev#548).
|
|
1302
|
+
*
|
|
1303
|
+
* `buildJavaTaintedVars` is file-wide and keyed by name, so a local `data`
|
|
1304
|
+
* tainted in one method tainted every `data` in the file. Here a derivation
|
|
1305
|
+
* `lhs = …tainted…` only counts inside the method where the tainted name lives.
|
|
1306
|
+
* Taint crosses methods in exactly two ways, both real data flow:
|
|
1307
|
+
* - a class field: `dataBad = data;` in one method, `x = dataBad;` in another;
|
|
1308
|
+
* - a return value: `return data;` in `Source()`, `x = Source();` in a caller.
|
|
1309
|
+
*
|
|
1310
|
+
* Returns one entry per derived name. `carried` entries entered their method
|
|
1311
|
+
* through a field read or a call, so their `line` (the derivation) is where
|
|
1312
|
+
* the taint starts in that method; the others derive from a source in the
|
|
1313
|
+
* same method.
|
|
1314
|
+
*/
|
|
1315
|
+
function csharpMethodScopedAliases(code, seeds, methods, fieldNames) {
|
|
1316
|
+
const lines = code.split('\n');
|
|
1317
|
+
const methodOf = (line) => {
|
|
1318
|
+
let best = -1;
|
|
1319
|
+
let span = Infinity;
|
|
1320
|
+
methods.forEach((m, i) => {
|
|
1321
|
+
if (line >= m.start_line && line <= m.end_line && m.end_line - m.start_line < span) {
|
|
1322
|
+
best = i;
|
|
1323
|
+
span = m.end_line - m.start_line;
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
return best;
|
|
1327
|
+
};
|
|
1328
|
+
const declRe = /^\s*(?:public|private|protected|internal|static|readonly|const|\s)*\s*(?:[A-Za-z_][\w.]*(?:\s*<[^>]*>)?(?:\s*\[\s*\])*\??)\s+([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
1329
|
+
const assignRe = /^\s*(?:this\s*\.\s*)?([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
1330
|
+
const returnRe = /^\s*return\s+(.+?);\s*$/;
|
|
1331
|
+
const KEYWORDS = new Set(['return', 'throw', 'new', 'await', 'yield', 'else', 'case', 'using', 'var']);
|
|
1332
|
+
const codeOf = (rhs) => rhs
|
|
1333
|
+
.replace(/\$@?"(?:[^"\\]|\\.)*"/g, lit => ' ' + [...lit.matchAll(/\{([^}]*)\}/g)].map(x => x[1]).join(' ') + ' ')
|
|
1334
|
+
.replace(/@?"(?:[^"\\]|\\.)*"/g, ' ')
|
|
1335
|
+
.replace(/'(?:[^'\\]|\\.)'/g, ' ');
|
|
1336
|
+
const local = new Map(); // method index -> tainted locals
|
|
1337
|
+
const taintedFields = new Set();
|
|
1338
|
+
const returnsTainted = new Set(); // method names
|
|
1339
|
+
const localsOf = (m) => {
|
|
1340
|
+
let set = local.get(m);
|
|
1341
|
+
if (!set) {
|
|
1342
|
+
set = new Set();
|
|
1343
|
+
local.set(m, set);
|
|
1344
|
+
}
|
|
1345
|
+
return set;
|
|
1346
|
+
};
|
|
1347
|
+
for (const seed of seeds) {
|
|
1348
|
+
const base = seed.variable.replace(/^this\./, '');
|
|
1349
|
+
if (fieldNames.has(base))
|
|
1350
|
+
taintedFields.add(base);
|
|
1351
|
+
else
|
|
1352
|
+
localsOf(methodOf(seed.line)).add(seed.variable);
|
|
1353
|
+
}
|
|
1354
|
+
const out = [];
|
|
1355
|
+
let changed = true;
|
|
1356
|
+
for (let guard = 0; changed && guard < lines.length + 2; guard++) {
|
|
1357
|
+
changed = false;
|
|
1358
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1359
|
+
let text = lines[i];
|
|
1360
|
+
const trimmed = text.trimStart();
|
|
1361
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*'))
|
|
1362
|
+
continue;
|
|
1363
|
+
// A statement wrapped over several lines (`string q = "…" + user +` /
|
|
1364
|
+
// `"…" + pass;`) is one assignment: join it up to its `;` so the names
|
|
1365
|
+
// on the continuation lines count (cognium-dev#513).
|
|
1366
|
+
if (/^\s*(?:[\w.<>\[\]?]+\s+)?[A-Za-z_]\w*\s*=(?!=)/.test(text) && !/;\s*(?:\/\/.*)?$/.test(text)) {
|
|
1367
|
+
for (let j = i + 1; j < lines.length && j <= i + 12; j++) {
|
|
1368
|
+
// trimEnd: a CRLF file leaves `\r` on each line, which `.` will not cross.
|
|
1369
|
+
text = text.trimEnd() + ' ' + lines[j].trim();
|
|
1370
|
+
if (/;\s*(?:\/\/.*)?$/.test(lines[j]))
|
|
1371
|
+
break;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
const m = methodOf(i + 1);
|
|
1375
|
+
const here = localsOf(m);
|
|
1376
|
+
const ret = returnRe.exec(text);
|
|
1377
|
+
if (ret && m !== -1 && !returnsTainted.has(methods[m].name)) {
|
|
1378
|
+
const idents = codeOf(ret[1]).match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
1379
|
+
if (idents.some(t => here.has(t) || taintedFields.has(t))) {
|
|
1380
|
+
returnsTainted.add(methods[m].name);
|
|
1381
|
+
changed = true;
|
|
1382
|
+
}
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
const d = declRe.exec(text) ?? assignRe.exec(text);
|
|
1386
|
+
if (!d)
|
|
1387
|
+
continue;
|
|
1388
|
+
const [, lhs, rhs] = d;
|
|
1389
|
+
if (KEYWORDS.has(lhs))
|
|
1390
|
+
continue;
|
|
1391
|
+
const isField = fieldNames.has(lhs) && !declRe.test(text);
|
|
1392
|
+
if (isField ? taintedFields.has(lhs) : here.has(lhs))
|
|
1393
|
+
continue;
|
|
1394
|
+
const rhsCode = codeOf(rhs);
|
|
1395
|
+
const idents = rhsCode.match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
1396
|
+
const viaLocal = idents.some(t => here.has(t));
|
|
1397
|
+
const viaField = idents.some(t => taintedFields.has(t) && !here.has(t));
|
|
1398
|
+
const viaCall = [...rhsCode.matchAll(/([\p{L}_][\p{L}\p{N}_]*)\s*\(/gu)].some(c => returnsTainted.has(c[1]));
|
|
1399
|
+
if (!viaLocal && !viaField && !viaCall)
|
|
1400
|
+
continue;
|
|
1401
|
+
if (isField)
|
|
1402
|
+
taintedFields.add(lhs);
|
|
1403
|
+
else
|
|
1404
|
+
here.add(lhs);
|
|
1405
|
+
out.push({ variable: lhs, line: i + 1, carried: !viaLocal });
|
|
1406
|
+
changed = true;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return out;
|
|
1410
|
+
}
|
|
1293
1411
|
let _reassignSplitCode;
|
|
1294
1412
|
let _reassignSplitLines;
|
|
1295
1413
|
function splitLinesCached(code) {
|
|
@@ -1299,7 +1417,7 @@ function splitLinesCached(code) {
|
|
|
1299
1417
|
_reassignSplitLines = code.split('\n');
|
|
1300
1418
|
return _reassignSplitLines;
|
|
1301
1419
|
}
|
|
1302
|
-
function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
1420
|
+
function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine, unreachableLines) {
|
|
1303
1421
|
if (!variable || sinkLine - srcLine < 2)
|
|
1304
1422
|
return false;
|
|
1305
1423
|
// Bare identifiers only — attribute paths like `obj.attr` are not
|
|
@@ -1340,56 +1458,145 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
|
1340
1458
|
// A literal assigned in one case of a switch only overwrites the value
|
|
1341
1459
|
// when no case gives the variable anything else. `case 'A': bar = param;`
|
|
1342
1460
|
// next to `default: bar = "safe";` leaves `bar` tainted on the 'A' path.
|
|
1343
|
-
if (
|
|
1461
|
+
if (siblingBranchAssignsNonLiteral(lines, i, variable, strLit, unreachableLines))
|
|
1344
1462
|
continue;
|
|
1345
1463
|
return true;
|
|
1346
1464
|
}
|
|
1347
1465
|
return false;
|
|
1348
1466
|
}
|
|
1349
|
-
/**
|
|
1350
|
-
|
|
1351
|
-
* assigns `variable` a value that is not a string literal. Brace counting on
|
|
1352
|
-
* raw text: good enough for the brace-delimited switch bodies of Java, JS/TS
|
|
1353
|
-
* and Go, and it answers false (keeping the literal as an overwrite) whenever
|
|
1354
|
-
* no enclosing switch is found.
|
|
1355
|
-
*/
|
|
1356
|
-
function switchAlsoAssignsNonLiteral(lines, idx, variable, strLit) {
|
|
1467
|
+
/** The `{` that encloses position `(line, col)`, with the statement that opens it. */
|
|
1468
|
+
function enclosingBrace(lines, line, col) {
|
|
1357
1469
|
let depth = 0;
|
|
1358
|
-
let
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
if (line[c] === '}')
|
|
1470
|
+
for (let i = line; i >= 0; i--) {
|
|
1471
|
+
const text = lines[i];
|
|
1472
|
+
for (let c = i === line ? Math.min(col, text.length) - 1 : text.length - 1; c >= 0; c--) {
|
|
1473
|
+
if (text[c] === '}')
|
|
1363
1474
|
depth++;
|
|
1364
|
-
else if (
|
|
1475
|
+
else if (text[c] === '{') {
|
|
1476
|
+
if (depth === 0) {
|
|
1477
|
+
// K&R: the statement precedes the brace on this line. Allman: the
|
|
1478
|
+
// brace stands alone and the statement is the line above it.
|
|
1479
|
+
let header = text.slice(0, c).trim();
|
|
1480
|
+
let headerLine = i;
|
|
1481
|
+
for (let h = i - 1; header === '' && h >= 0; h--) {
|
|
1482
|
+
header = lines[h].trim();
|
|
1483
|
+
headerLine = h;
|
|
1484
|
+
}
|
|
1485
|
+
return { line: i, col: c, header, headerLine };
|
|
1486
|
+
}
|
|
1365
1487
|
depth--;
|
|
1488
|
+
}
|
|
1366
1489
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1490
|
+
}
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
/** The `}` matching the `{` at `(line, col)`. */
|
|
1494
|
+
function matchingClose(lines, line, col) {
|
|
1495
|
+
let depth = 0;
|
|
1496
|
+
for (let i = line; i < lines.length; i++) {
|
|
1497
|
+
const text = lines[i];
|
|
1498
|
+
for (let c = i === line ? col : 0; c < text.length; c++) {
|
|
1499
|
+
if (text[c] === '{')
|
|
1500
|
+
depth++;
|
|
1501
|
+
else if (text[c] === '}' && --depth === 0)
|
|
1502
|
+
return { line: i, col: c };
|
|
1371
1503
|
}
|
|
1372
1504
|
}
|
|
1373
|
-
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* True when line `idx` assigns `variable` in one arm of a `switch` or one
|
|
1509
|
+
* branch of an `if`/`else` chain, and a sibling arm or branch assigns it a
|
|
1510
|
+
* value that is not a string literal.
|
|
1511
|
+
*
|
|
1512
|
+
* A literal in one branch does not overwrite what a sibling branch assigned:
|
|
1513
|
+
* `if (c) { data = input; } else { data = "foo"; }` leaves `data` tainted on
|
|
1514
|
+
* one path, so the literal must not suppress the flow (cognium-dev#513, #582).
|
|
1515
|
+
* A sibling assignment on a line constant propagation proved unreachable does
|
|
1516
|
+
* not count, which keeps `if (false) { x = input; } else { x = "safe"; }` clean.
|
|
1517
|
+
*
|
|
1518
|
+
* Works on raw text by brace matching, for brace-delimited bodies in either
|
|
1519
|
+
* brace style. It answers false, keeping the literal as an overwrite, when
|
|
1520
|
+
* the line is not inside such a construct or a branch has no braces.
|
|
1521
|
+
*/
|
|
1522
|
+
function siblingBranchAssignsNonLiteral(lines, idx, variable, strLit, unreachableLines) {
|
|
1523
|
+
let block = enclosingBrace(lines, idx, 0);
|
|
1524
|
+
// A braced `case 6: { … }` arm: step out to the switch body.
|
|
1525
|
+
if (block && /^(?:case\b.*|default\s*):$/.test(block.header)) {
|
|
1526
|
+
block = enclosingBrace(lines, block.line, block.col);
|
|
1527
|
+
}
|
|
1528
|
+
if (!block)
|
|
1374
1529
|
return false;
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1530
|
+
let first = block.headerLine;
|
|
1531
|
+
let last;
|
|
1532
|
+
const end = matchingClose(lines, block.line, block.col);
|
|
1533
|
+
if (!end)
|
|
1534
|
+
return false;
|
|
1535
|
+
last = end.line;
|
|
1536
|
+
if (/\bswitch\b/.test(block.header)) {
|
|
1537
|
+
// the whole switch body
|
|
1538
|
+
}
|
|
1539
|
+
else if (/^(?:\}\s*)?else\b/.test(block.header) || /^(?:\}\s*)?(?:else\s+)?if\s*\(/.test(block.header)) {
|
|
1540
|
+
// Walk up through `else` / `else if` to the opening `if`.
|
|
1541
|
+
let cur = block;
|
|
1542
|
+
for (let guard = 0; guard < 64 && /^(?:\}\s*)?else\b/.test(cur.header); guard++) {
|
|
1543
|
+
// The previous branch closes on the header line (`} else {`) or on the
|
|
1544
|
+
// last non-blank line above it.
|
|
1545
|
+
let closeLine = cur.headerLine;
|
|
1546
|
+
let closeCol = lines[closeLine].indexOf('}');
|
|
1547
|
+
if (closeCol === -1 || closeCol > lines[closeLine].search(/\belse\b/)) {
|
|
1548
|
+
closeLine--;
|
|
1549
|
+
while (closeLine >= 0 && lines[closeLine].trim() === '')
|
|
1550
|
+
closeLine--;
|
|
1551
|
+
if (closeLine < 0 || !lines[closeLine].trimEnd().endsWith('}'))
|
|
1552
|
+
return false;
|
|
1553
|
+
closeCol = lines[closeLine].lastIndexOf('}');
|
|
1554
|
+
}
|
|
1555
|
+
const prev = enclosingBrace(lines, closeLine, closeCol);
|
|
1556
|
+
if (!prev)
|
|
1557
|
+
return false;
|
|
1558
|
+
cur = prev;
|
|
1559
|
+
first = prev.headerLine;
|
|
1384
1560
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1561
|
+
// Walk down through following `else` branches.
|
|
1562
|
+
let close = end;
|
|
1563
|
+
for (let guard = 0; guard < 64; guard++) {
|
|
1564
|
+
let l = close.line;
|
|
1565
|
+
let rest = lines[l].slice(close.col + 1).trim();
|
|
1566
|
+
while (rest === '' && l + 1 < lines.length) {
|
|
1567
|
+
l++;
|
|
1568
|
+
rest = lines[l].trim();
|
|
1569
|
+
}
|
|
1570
|
+
if (!/^else\b/.test(rest))
|
|
1571
|
+
break;
|
|
1572
|
+
let openLine = l;
|
|
1573
|
+
let openCol = lines[l].indexOf('{', l === close.line ? close.col + 1 : 0);
|
|
1574
|
+
if (openCol === -1) {
|
|
1575
|
+
openLine = l + 1;
|
|
1576
|
+
while (openLine < lines.length && lines[openLine].trim() === '')
|
|
1577
|
+
openLine++;
|
|
1578
|
+
if (openLine >= lines.length || !lines[openLine].trimStart().startsWith('{'))
|
|
1579
|
+
break;
|
|
1580
|
+
openCol = lines[openLine].indexOf('{');
|
|
1581
|
+
}
|
|
1582
|
+
const next = matchingClose(lines, openLine, openCol);
|
|
1583
|
+
if (!next)
|
|
1584
|
+
break;
|
|
1585
|
+
close = next;
|
|
1586
|
+
last = next.line;
|
|
1390
1587
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
return false;
|
|
1591
|
+
}
|
|
1592
|
+
const assign = new RegExp(`(?:^|[^\\w.$])${variable}\\s*(?::?=)(?!=)\\s*(.*?)\\s*;?\\s*(?:break\\s*;?)?\\s*$`);
|
|
1593
|
+
const literalOnly = new RegExp(`^${strLit}$`);
|
|
1594
|
+
for (let i = first; i <= last; i++) {
|
|
1595
|
+
if (i === idx || unreachableLines?.has(i + 1))
|
|
1596
|
+
continue;
|
|
1597
|
+
const m = assign.exec(lines[i]);
|
|
1598
|
+
if (m && m[1] && !literalOnly.test(m[1]))
|
|
1599
|
+
return true;
|
|
1393
1600
|
}
|
|
1394
1601
|
return false;
|
|
1395
1602
|
}
|
|
@@ -1600,6 +1807,26 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
1600
1807
|
// is every Juliet C# file.
|
|
1601
1808
|
const csFnRanges = methodRanges(['csharp']);
|
|
1602
1809
|
const csFnStart = (line) => enclosingStart(csFnRanges, line);
|
|
1810
|
+
// cognium-dev#548 — names declared as class fields. A field is the one kind
|
|
1811
|
+
// of variable whose value legitimately crosses from one method to another, so
|
|
1812
|
+
// the per-method gate below must not apply to it.
|
|
1813
|
+
const csFieldNames = new Set();
|
|
1814
|
+
if (language === 'csharp') {
|
|
1815
|
+
for (const t of types ?? [])
|
|
1816
|
+
for (const f of t.fields ?? [])
|
|
1817
|
+
csFieldNames.add(f.name);
|
|
1818
|
+
}
|
|
1819
|
+
/** True when a C# source and a later line can only be linked by a reused local name. */
|
|
1820
|
+
const csDifferentMethod = (variable, sourceLine, otherLine) => {
|
|
1821
|
+
if (csFnRanges.length === 0)
|
|
1822
|
+
return false;
|
|
1823
|
+
const a = csFnStart(sourceLine);
|
|
1824
|
+
const b = csFnStart(otherLine);
|
|
1825
|
+
if (a === -1 || b === -1 || a === b)
|
|
1826
|
+
return false;
|
|
1827
|
+
const base = variable.replace(/^this\./, '').split('.')[0];
|
|
1828
|
+
return !csFieldNames.has(base);
|
|
1829
|
+
};
|
|
1603
1830
|
// Variable-name scan path: only consider sources that carry an explicit
|
|
1604
1831
|
// variable name. The colocation path below (cognium-dev #83) runs even
|
|
1605
1832
|
// when this set is empty, so we no longer early-return.
|
|
@@ -2000,7 +2227,39 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2000
2227
|
// the array literal defeats the sink-arg colocation heuristic. Seeds
|
|
2001
2228
|
// with real source variables (HTTP source `arg` or the now-populated
|
|
2002
2229
|
// interprocedural_param parameter name) and iterates to a fixpoint.
|
|
2003
|
-
if (
|
|
2230
|
+
if (language === 'csharp' && typeof code === 'string' && sourcesWithVar.length > 0 && csFnRanges.length > 0) {
|
|
2231
|
+
// cognium-dev#548: derive aliases per method. A name tainted in one method
|
|
2232
|
+
// says nothing about the same name in another; taint reaches another
|
|
2233
|
+
// method only through a field or a return value.
|
|
2234
|
+
const csMethods = (types ?? []).flatMap(t => t.methods);
|
|
2235
|
+
const seeds = sourcesWithVar.map(sv => ({ variable: sv.variable, line: sv.line }));
|
|
2236
|
+
const methodStartOf = (line) => csFnStart(line);
|
|
2237
|
+
const aliases = csharpMethodScopedAliases(code, seeds, csMethods, csFieldNames);
|
|
2238
|
+
const fileAnchor = sourcesWithVar.reduce((a, sv) => (sv.line < a.line ? sv : a), sourcesWithVar[0]);
|
|
2239
|
+
const have = new Set(sourcesWithVar.map(sv => `${sv.variable}@${methodStartOf(sv.line)}`));
|
|
2240
|
+
for (const alias of aliases) {
|
|
2241
|
+
const fn = methodStartOf(alias.line);
|
|
2242
|
+
const isField = csFieldNames.has(alias.variable);
|
|
2243
|
+
const key = `${alias.variable}@${isField ? 'field' : fn}`;
|
|
2244
|
+
if (have.has(key))
|
|
2245
|
+
continue;
|
|
2246
|
+
have.add(key);
|
|
2247
|
+
// A same-method derivation reports the source it derives from. A carried
|
|
2248
|
+
// one starts where the value enters this method (the field read or the
|
|
2249
|
+
// call), which also keeps it ahead of a sink in a method declared
|
|
2250
|
+
// earlier in the file than the method holding the source.
|
|
2251
|
+
const inMethod = sourcesWithVar.filter(sv => methodStartOf(sv.line) === fn && sv.line <= alias.line);
|
|
2252
|
+
const from = !alias.carried && inMethod.length > 0
|
|
2253
|
+
? inMethod.reduce((a, sv) => (sv.line < a.line ? sv : a), inMethod[0])
|
|
2254
|
+
: fileAnchor;
|
|
2255
|
+
sourcesWithVar.push({
|
|
2256
|
+
...from,
|
|
2257
|
+
variable: alias.variable,
|
|
2258
|
+
...(alias.carried || inMethod.length === 0 ? { line: alias.line } : {}),
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
else if ((language === 'java' || language === 'csharp') && typeof code === 'string' && sourcesWithVar.length > 0) {
|
|
2004
2263
|
const seedVars = new Set(sourcesWithVar.map(s => s.variable));
|
|
2005
2264
|
const derived = buildJavaTaintedVars(code, seedVars);
|
|
2006
2265
|
if (derived.size > 0) {
|
|
@@ -2065,23 +2324,31 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2065
2324
|
if (language === 'csharp' && typeof code === 'string' && sourcesWithVar.length > 0) {
|
|
2066
2325
|
const lines = code.split('\n');
|
|
2067
2326
|
const ctRe = /(\w+)\s*\.\s*CommandText\s*\+?=\s*(.+)/;
|
|
2068
|
-
|
|
2327
|
+
// cognium-dev#548: seed per method, from sources in that method. The seed
|
|
2328
|
+
// used to be made once per file and matched by name, so `cmd.CommandText =
|
|
2329
|
+
// … + data` in `Bad()` tainted `cmd.ExecuteScalar()` in `GoodG2B()`, whose
|
|
2330
|
+
// own `data` is a constant: all 27 Juliet CWE-89 false positives. Once per
|
|
2331
|
+
// file also meant a second vulnerable method reusing the name got no seed.
|
|
2332
|
+
const seeded = new Set();
|
|
2069
2333
|
let added = true;
|
|
2070
2334
|
let guard = 0;
|
|
2071
2335
|
while (added && guard < lines.length + 2) {
|
|
2072
2336
|
added = false;
|
|
2073
2337
|
guard++;
|
|
2074
|
-
const known = new Set(sourcesWithVar.map(s => s.variable));
|
|
2075
2338
|
for (let i = 0; i < lines.length; i++) {
|
|
2076
2339
|
const m = ctRe.exec(lines[i]);
|
|
2077
2340
|
if (!m)
|
|
2078
2341
|
continue;
|
|
2079
2342
|
const [, obj, rhs] = m;
|
|
2080
|
-
|
|
2343
|
+
const key = `${obj}@${csFnStart(i + 1)}`;
|
|
2344
|
+
if (seeded.has(key))
|
|
2081
2345
|
continue;
|
|
2082
|
-
|
|
2346
|
+
const reachable = sourcesWithVar.filter(sv => sv.line <= i + 1 && !csDifferentMethod(sv.variable, sv.line, i + 1));
|
|
2347
|
+
if (reachable.length === 0 || !argsReferenceTaintedVar(rhs, reachable))
|
|
2083
2348
|
continue;
|
|
2084
|
-
|
|
2349
|
+
const from = reachable.reduce((a, sv) => (sv.line < a.line ? sv : a), reachable[0]);
|
|
2350
|
+
sourcesWithVar.push({ ...from, variable: obj, line: i + 1 });
|
|
2351
|
+
seeded.add(key);
|
|
2085
2352
|
added = true;
|
|
2086
2353
|
}
|
|
2087
2354
|
}
|
|
@@ -2123,18 +2390,21 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2123
2390
|
// way would be a guess.
|
|
2124
2391
|
const appendRe = /([\p{L}_][\p{L}\p{N}_]*)\s*\.\s*(?:Append|AppendLine|AppendFormat|Insert)\s*\(([\s\S]*)/u;
|
|
2125
2392
|
const anchor = sourcesWithVar.reduce((a, s) => (s.line < a.line ? s : a), sourcesWithVar[0]);
|
|
2393
|
+
const builderSeeded = new Set();
|
|
2126
2394
|
let added = true;
|
|
2127
2395
|
let guard = 0;
|
|
2128
2396
|
while (added && guard < lines.length + 2) {
|
|
2129
2397
|
added = false;
|
|
2130
2398
|
guard++;
|
|
2131
|
-
const known = new Set(sourcesWithVar.map(s => s.variable));
|
|
2132
2399
|
for (let i = 0; i < lines.length; i++) {
|
|
2133
2400
|
const m = appendRe.exec(lines[i]);
|
|
2134
2401
|
if (!m)
|
|
2135
2402
|
continue;
|
|
2136
2403
|
const [, obj, args] = m;
|
|
2137
|
-
|
|
2404
|
+
// Keyed per method (#548): one seed per file skipped a second
|
|
2405
|
+
// method that appends tainted text to a builder of the same name.
|
|
2406
|
+
const seedKey = `${obj}@${csFnStart(i + 1)}`;
|
|
2407
|
+
if (!builders.has(obj) || builderSeeded.has(seedKey))
|
|
2138
2408
|
continue;
|
|
2139
2409
|
// Same-method gate. `sourcesWithVar` is file-wide, so without this a
|
|
2140
2410
|
// source named `data` in one method taints a builder appended to in
|
|
@@ -2146,6 +2416,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2146
2416
|
if (reachable.length > 0 && argsReferenceTaintedVar(args, reachable)) {
|
|
2147
2417
|
sourcesWithVar.push({ ...anchor, variable: obj, line: i + 1 });
|
|
2148
2418
|
builderSeeds.add(`${obj}@${i + 1}`);
|
|
2419
|
+
builderSeeded.add(seedKey);
|
|
2149
2420
|
added = true;
|
|
2150
2421
|
}
|
|
2151
2422
|
}
|
|
@@ -2291,6 +2562,12 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2291
2562
|
if (srcFn !== sinkFn)
|
|
2292
2563
|
continue;
|
|
2293
2564
|
}
|
|
2565
|
+
// cognium-dev#548: the same gate for every C# source. Two locals of
|
|
2566
|
+
// the same name in different methods are different variables; a
|
|
2567
|
+
// value that really crosses methods does so through a field
|
|
2568
|
+
// (exempt), a parameter or a return, never through a name match.
|
|
2569
|
+
if (csDifferentMethod(source.variable, source.line, sink.line))
|
|
2570
|
+
continue;
|
|
2294
2571
|
// Simple-identifier sources are already confirmed present by the
|
|
2295
2572
|
// token index; only complex (attribute-path) sources need the
|
|
2296
2573
|
// word-boundary regex to confirm they actually occur in the arg.
|
|
@@ -2327,7 +2604,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2327
2604
|
// `if var not in ALLOWLIST: var = "lit"`), the original taint
|
|
2328
2605
|
// no longer reaches the sink — suppress the flow.
|
|
2329
2606
|
if (typeof code === 'string' &&
|
|
2330
|
-
isReassignedToLiteralBetween(code, source.variable, source.line, sink.line)) {
|
|
2607
|
+
isReassignedToLiteralBetween(code, source.variable, source.line, sink.line, unreachableLines)) {
|
|
2331
2608
|
break;
|
|
2332
2609
|
}
|
|
2333
2610
|
// Note: DFG-flow filter handles sanitizer-guard suppression
|