circle-ir 4.11.0 → 4.13.0
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/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +8 -2
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/constant-propagation/propagator.d.ts +2 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +56 -2
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/entry-point-detection.d.ts +7 -0
- package/dist/analysis/entry-point-detection.d.ts.map +1 -1
- package/dist/analysis/entry-point-detection.js +13 -1
- package/dist/analysis/entry-point-detection.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 +147 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.js +564 -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 +89 -0
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +6 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +15 -2
- package/dist/analysis/passes/language-sources-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 +49 -52
- 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 +348 -16
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/require-entry-path.js +50 -4
- package/dist/analysis/require-entry-path.js.map +1 -1
- package/dist/browser/circle-ir.js +861 -65
- package/dist/core/circle-ir-core.cjs +81 -4
- package/dist/core/circle-ir-core.js +81 -4
- package/dist/core/extractors/dfg.js +49 -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
|
|
@@ -1333,7 +1451,151 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
|
1333
1451
|
const line = lines[i];
|
|
1334
1452
|
if (!line)
|
|
1335
1453
|
continue;
|
|
1336
|
-
if (
|
|
1454
|
+
if (reGuarded.test(line))
|
|
1455
|
+
return true;
|
|
1456
|
+
if (!reNaked.test(line) && !reSwitchCase.test(line))
|
|
1457
|
+
continue;
|
|
1458
|
+
// A literal assigned in one case of a switch only overwrites the value
|
|
1459
|
+
// when no case gives the variable anything else. `case 'A': bar = param;`
|
|
1460
|
+
// next to `default: bar = "safe";` leaves `bar` tainted on the 'A' path.
|
|
1461
|
+
if (siblingBranchAssignsNonLiteral(lines, i, variable, strLit, unreachableLines))
|
|
1462
|
+
continue;
|
|
1463
|
+
return true;
|
|
1464
|
+
}
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
/** The `{` that encloses position `(line, col)`, with the statement that opens it. */
|
|
1468
|
+
function enclosingBrace(lines, line, col) {
|
|
1469
|
+
let depth = 0;
|
|
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] === '}')
|
|
1474
|
+
depth++;
|
|
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
|
+
}
|
|
1487
|
+
depth--;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
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 };
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
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)
|
|
1529
|
+
return false;
|
|
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;
|
|
1560
|
+
}
|
|
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;
|
|
1587
|
+
}
|
|
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]))
|
|
1337
1599
|
return true;
|
|
1338
1600
|
}
|
|
1339
1601
|
return false;
|
|
@@ -1545,6 +1807,26 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
1545
1807
|
// is every Juliet C# file.
|
|
1546
1808
|
const csFnRanges = methodRanges(['csharp']);
|
|
1547
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
|
+
};
|
|
1548
1830
|
// Variable-name scan path: only consider sources that carry an explicit
|
|
1549
1831
|
// variable name. The colocation path below (cognium-dev #83) runs even
|
|
1550
1832
|
// when this set is empty, so we no longer early-return.
|
|
@@ -1945,7 +2227,39 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
1945
2227
|
// the array literal defeats the sink-arg colocation heuristic. Seeds
|
|
1946
2228
|
// with real source variables (HTTP source `arg` or the now-populated
|
|
1947
2229
|
// interprocedural_param parameter name) and iterates to a fixpoint.
|
|
1948
|
-
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) {
|
|
1949
2263
|
const seedVars = new Set(sourcesWithVar.map(s => s.variable));
|
|
1950
2264
|
const derived = buildJavaTaintedVars(code, seedVars);
|
|
1951
2265
|
if (derived.size > 0) {
|
|
@@ -2010,23 +2324,31 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2010
2324
|
if (language === 'csharp' && typeof code === 'string' && sourcesWithVar.length > 0) {
|
|
2011
2325
|
const lines = code.split('\n');
|
|
2012
2326
|
const ctRe = /(\w+)\s*\.\s*CommandText\s*\+?=\s*(.+)/;
|
|
2013
|
-
|
|
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();
|
|
2014
2333
|
let added = true;
|
|
2015
2334
|
let guard = 0;
|
|
2016
2335
|
while (added && guard < lines.length + 2) {
|
|
2017
2336
|
added = false;
|
|
2018
2337
|
guard++;
|
|
2019
|
-
const known = new Set(sourcesWithVar.map(s => s.variable));
|
|
2020
2338
|
for (let i = 0; i < lines.length; i++) {
|
|
2021
2339
|
const m = ctRe.exec(lines[i]);
|
|
2022
2340
|
if (!m)
|
|
2023
2341
|
continue;
|
|
2024
2342
|
const [, obj, rhs] = m;
|
|
2025
|
-
|
|
2343
|
+
const key = `${obj}@${csFnStart(i + 1)}`;
|
|
2344
|
+
if (seeded.has(key))
|
|
2026
2345
|
continue;
|
|
2027
|
-
|
|
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))
|
|
2028
2348
|
continue;
|
|
2029
|
-
|
|
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);
|
|
2030
2352
|
added = true;
|
|
2031
2353
|
}
|
|
2032
2354
|
}
|
|
@@ -2068,18 +2390,21 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2068
2390
|
// way would be a guess.
|
|
2069
2391
|
const appendRe = /([\p{L}_][\p{L}\p{N}_]*)\s*\.\s*(?:Append|AppendLine|AppendFormat|Insert)\s*\(([\s\S]*)/u;
|
|
2070
2392
|
const anchor = sourcesWithVar.reduce((a, s) => (s.line < a.line ? s : a), sourcesWithVar[0]);
|
|
2393
|
+
const builderSeeded = new Set();
|
|
2071
2394
|
let added = true;
|
|
2072
2395
|
let guard = 0;
|
|
2073
2396
|
while (added && guard < lines.length + 2) {
|
|
2074
2397
|
added = false;
|
|
2075
2398
|
guard++;
|
|
2076
|
-
const known = new Set(sourcesWithVar.map(s => s.variable));
|
|
2077
2399
|
for (let i = 0; i < lines.length; i++) {
|
|
2078
2400
|
const m = appendRe.exec(lines[i]);
|
|
2079
2401
|
if (!m)
|
|
2080
2402
|
continue;
|
|
2081
2403
|
const [, obj, args] = m;
|
|
2082
|
-
|
|
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))
|
|
2083
2408
|
continue;
|
|
2084
2409
|
// Same-method gate. `sourcesWithVar` is file-wide, so without this a
|
|
2085
2410
|
// source named `data` in one method taints a builder appended to in
|
|
@@ -2091,6 +2416,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2091
2416
|
if (reachable.length > 0 && argsReferenceTaintedVar(args, reachable)) {
|
|
2092
2417
|
sourcesWithVar.push({ ...anchor, variable: obj, line: i + 1 });
|
|
2093
2418
|
builderSeeds.add(`${obj}@${i + 1}`);
|
|
2419
|
+
builderSeeded.add(seedKey);
|
|
2094
2420
|
added = true;
|
|
2095
2421
|
}
|
|
2096
2422
|
}
|
|
@@ -2236,6 +2562,12 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2236
2562
|
if (srcFn !== sinkFn)
|
|
2237
2563
|
continue;
|
|
2238
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;
|
|
2239
2571
|
// Simple-identifier sources are already confirmed present by the
|
|
2240
2572
|
// token index; only complex (attribute-path) sources need the
|
|
2241
2573
|
// word-boundary regex to confirm they actually occur in the arg.
|
|
@@ -2272,7 +2604,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
2272
2604
|
// `if var not in ALLOWLIST: var = "lit"`), the original taint
|
|
2273
2605
|
// no longer reaches the sink — suppress the flow.
|
|
2274
2606
|
if (typeof code === 'string' &&
|
|
2275
|
-
isReassignedToLiteralBetween(code, source.variable, source.line, sink.line)) {
|
|
2607
|
+
isReassignedToLiteralBetween(code, source.variable, source.line, sink.line, unreachableLines)) {
|
|
2276
2608
|
break;
|
|
2277
2609
|
}
|
|
2278
2610
|
// Note: DFG-flow filter handles sanitizer-guard suppression
|