circle-ir 3.195.0 → 3.198.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/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +523 -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 +68 -1
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.js.map +1 -1
- package/dist/analysis/rules.d.ts.map +1 -1
- package/dist/analysis/rules.js +42 -0
- package/dist/analysis/rules.js.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +15 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +366 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/languages/types.d.ts +3 -3
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/types/index.d.ts +23 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +77 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"language-sources-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/language-sources-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAwB,WAAW,EAAO,MAAM,sBAAsB,CAAC;AAC3H,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAqB9E,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;IA4E/B,CAAC;AAwDF,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,EAAE,WAAW,EAAE,CAAC;IACjC,eAAe,EAAE,SAAS,EAAE,CAAC;IAC7B;;;;OAIG;IACH,oBAAoB,EAAE,cAAc,EAAE,CAAC;IACvC;;;OAGG;IACH,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC;;;OAGG;IACH,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B;;;OAGG;IACH,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAMD,qBAAa,mBAAoB,YAAW,YAAY,CAAC,qBAAqB,CAAC;IAC7E,QAAQ,CAAC,IAAI,sBAAsB;IACnC,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,qBAAqB;
|
|
1
|
+
{"version":3,"file":"language-sources-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/language-sources-pass.ts"],"names":[],"mappings":"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"}
|
|
@@ -313,6 +313,9 @@ export class LanguageSourcesPass {
|
|
|
313
313
|
additionalSanitizers.push(...findPythonRegexAllowlistWrapperSanitizers(code));
|
|
314
314
|
additionalSanitizers.push(...findPythonSetMembershipXssGuardSanitizers(code));
|
|
315
315
|
additionalSanitizers.push(...findPythonTraversalRejectGuardSanitizers(code));
|
|
316
|
+
additionalSanitizers.push(...findPythonStringLiteralGuardSanitizers(code));
|
|
317
|
+
additionalSanitizers.push(...findPythonDefaultSaxParserXxeSanitizers(code));
|
|
318
|
+
additionalSanitizers.push(...findPythonUrlAllowlistRedirectSanitizers(code));
|
|
316
319
|
additionalSanitizers.push(...findPythonDefusedXmlSanitizers(code));
|
|
317
320
|
// Sprint 77a (#216 Pattern X): Jinja2 Environment(autoescape=...) +
|
|
318
321
|
// .render(...) sanitizer.
|
|
@@ -1165,14 +1168,205 @@ function findPythonAssignmentSources(sourceCode, language) {
|
|
|
1165
1168
|
}
|
|
1166
1169
|
return sources;
|
|
1167
1170
|
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Evaluate an integer arithmetic / comparison expression built only from
|
|
1173
|
+
* literals, known integer constants, `+ - * / // %`, parentheses and one
|
|
1174
|
+
* comparison operator. Returns `undefined` for anything it cannot decide —
|
|
1175
|
+
* an unknown name, a non-integer, a call, a string, division by zero.
|
|
1176
|
+
*
|
|
1177
|
+
* Deliberately hand-rolled rather than `eval`-ed: the input is untrusted
|
|
1178
|
+
* source text, and the analyzer must stay browser-safe.
|
|
1179
|
+
*/
|
|
1180
|
+
function evaluatePythonConstExpression(expr, consts) {
|
|
1181
|
+
const COMPARISON = /(<=|>=|==|!=|<|>)/;
|
|
1182
|
+
const parts = expr.split(COMPARISON);
|
|
1183
|
+
if (parts.length === 3) {
|
|
1184
|
+
const left = evaluatePythonConstExpression(parts[0], consts);
|
|
1185
|
+
const right = evaluatePythonConstExpression(parts[2], consts);
|
|
1186
|
+
if (typeof left !== 'number' || typeof right !== 'number')
|
|
1187
|
+
return undefined;
|
|
1188
|
+
switch (parts[1]) {
|
|
1189
|
+
case '<': return left < right;
|
|
1190
|
+
case '>': return left > right;
|
|
1191
|
+
case '<=': return left <= right;
|
|
1192
|
+
case '>=': return left >= right;
|
|
1193
|
+
case '==': return left === right;
|
|
1194
|
+
case '!=': return left !== right;
|
|
1195
|
+
default: return undefined;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (parts.length !== 1)
|
|
1199
|
+
return undefined;
|
|
1200
|
+
const matched = expr.match(/\d+|[A-Za-z_][A-Za-z0-9_]*|\/\/|[-+*/%()]|\S/g);
|
|
1201
|
+
if (!matched)
|
|
1202
|
+
return undefined;
|
|
1203
|
+
// Explicit binding so the hoisted `parseSum` declaration keeps the
|
|
1204
|
+
// non-null narrowing.
|
|
1205
|
+
const tokens = matched;
|
|
1206
|
+
let pos = 0;
|
|
1207
|
+
const peek = () => tokens[pos];
|
|
1208
|
+
// term := factor (('*' | '/' | '//' | '%') factor)*
|
|
1209
|
+
// sum := term (('+' | '-') term)*
|
|
1210
|
+
const parseFactor = () => {
|
|
1211
|
+
const tok = tokens[pos++];
|
|
1212
|
+
if (tok === undefined)
|
|
1213
|
+
return undefined;
|
|
1214
|
+
if (tok === '(') {
|
|
1215
|
+
const inner = parseSum();
|
|
1216
|
+
if (tokens[pos++] !== ')')
|
|
1217
|
+
return undefined;
|
|
1218
|
+
return inner;
|
|
1219
|
+
}
|
|
1220
|
+
if (tok === '-') {
|
|
1221
|
+
const operand = parseFactor();
|
|
1222
|
+
return operand === undefined ? undefined : -operand;
|
|
1223
|
+
}
|
|
1224
|
+
if (/^\d+$/.test(tok))
|
|
1225
|
+
return Number(tok);
|
|
1226
|
+
if (/^[A-Za-z_]/.test(tok))
|
|
1227
|
+
return consts.get(tok);
|
|
1228
|
+
return undefined;
|
|
1229
|
+
};
|
|
1230
|
+
const parseTerm = () => {
|
|
1231
|
+
let value = parseFactor();
|
|
1232
|
+
if (value === undefined)
|
|
1233
|
+
return undefined;
|
|
1234
|
+
while (peek() === '*' || peek() === '/' || peek() === '//' || peek() === '%') {
|
|
1235
|
+
const op = tokens[pos++];
|
|
1236
|
+
const rhs = parseFactor();
|
|
1237
|
+
if (rhs === undefined)
|
|
1238
|
+
return undefined;
|
|
1239
|
+
if ((op === '/' || op === '//' || op === '%') && rhs === 0)
|
|
1240
|
+
return undefined;
|
|
1241
|
+
if (op === '*')
|
|
1242
|
+
value *= rhs;
|
|
1243
|
+
else if (op === '/')
|
|
1244
|
+
value /= rhs;
|
|
1245
|
+
else if (op === '//')
|
|
1246
|
+
value = Math.floor(value / rhs);
|
|
1247
|
+
else
|
|
1248
|
+
value = ((value % rhs) + rhs) % rhs;
|
|
1249
|
+
}
|
|
1250
|
+
return value;
|
|
1251
|
+
};
|
|
1252
|
+
function parseSum() {
|
|
1253
|
+
let value = parseTerm();
|
|
1254
|
+
if (value === undefined)
|
|
1255
|
+
return undefined;
|
|
1256
|
+
while (peek() === '+' || peek() === '-') {
|
|
1257
|
+
const op = tokens[pos++];
|
|
1258
|
+
const rhs = parseTerm();
|
|
1259
|
+
if (rhs === undefined)
|
|
1260
|
+
return undefined;
|
|
1261
|
+
value = op === '+' ? value + rhs : value - rhs;
|
|
1262
|
+
}
|
|
1263
|
+
return value;
|
|
1264
|
+
}
|
|
1265
|
+
const result = parseSum();
|
|
1266
|
+
return pos === tokens.length ? result : undefined;
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Resolve a Python list index (which may be negative) against a known length.
|
|
1270
|
+
* Returns `undefined` when it falls outside the list, in which case the caller
|
|
1271
|
+
* stops tracking that list rather than guessing.
|
|
1272
|
+
*/
|
|
1273
|
+
function normalizeListIndex(index, length) {
|
|
1274
|
+
const resolved = index < 0 ? length + index : index;
|
|
1275
|
+
return resolved >= 0 && resolved < length ? resolved : undefined;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Line indices (0-based) belonging to a branch that cannot execute, because
|
|
1279
|
+
* its `if` condition is decidable from tracked integer constants:
|
|
1280
|
+
*
|
|
1281
|
+
* num = 86
|
|
1282
|
+
* if 7 * 42 - num > 200: # 208 > 200 → always true
|
|
1283
|
+
* bar = 'safe'
|
|
1284
|
+
* else:
|
|
1285
|
+
* bar = param # dead — must not seed taint
|
|
1286
|
+
*
|
|
1287
|
+
* The statement-level sibling of the conditional-expression folding in
|
|
1288
|
+
* `buildPythonTaintedVars`. Same evaluator, same conservatism: an undecidable
|
|
1289
|
+
* condition marks nothing dead.
|
|
1290
|
+
*
|
|
1291
|
+
* `elif` chains are skipped entirely — deciding one arm says nothing about the
|
|
1292
|
+
* rest without evaluating every preceding condition, and the corpus shapes
|
|
1293
|
+
* this targets are plain if/else.
|
|
1294
|
+
*/
|
|
1295
|
+
function computeDeadPythonBranchLines(lines) {
|
|
1296
|
+
const dead = new Set();
|
|
1297
|
+
const consts = new Map();
|
|
1298
|
+
const indentOf = (line) => line.length - line.trimStart().length;
|
|
1299
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1300
|
+
const line = lines[i];
|
|
1301
|
+
const assign = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(?!=)\s*(.+)$/);
|
|
1302
|
+
if (assign) {
|
|
1303
|
+
const value = evaluatePythonConstExpression(assign[2].trim(), consts);
|
|
1304
|
+
if (typeof value === 'number')
|
|
1305
|
+
consts.set(assign[1], value);
|
|
1306
|
+
else
|
|
1307
|
+
consts.delete(assign[1]);
|
|
1308
|
+
}
|
|
1309
|
+
const ifMatch = line.match(/^(\s*)if\s+(.+?)\s*:\s*$/);
|
|
1310
|
+
if (!ifMatch)
|
|
1311
|
+
continue;
|
|
1312
|
+
const indent = ifMatch[1].length;
|
|
1313
|
+
const decided = evaluatePythonConstExpression(ifMatch[2].trim(), consts);
|
|
1314
|
+
if (decided !== true && decided !== false)
|
|
1315
|
+
continue;
|
|
1316
|
+
// Then-block: everything more deeply indented than the `if`.
|
|
1317
|
+
const thenStart = i + 1;
|
|
1318
|
+
let cursor = thenStart;
|
|
1319
|
+
while (cursor < lines.length && (lines[cursor].trim() === '' || indentOf(lines[cursor]) > indent)) {
|
|
1320
|
+
cursor++;
|
|
1321
|
+
}
|
|
1322
|
+
const thenEnd = cursor - 1;
|
|
1323
|
+
let elseStart = -1;
|
|
1324
|
+
let elseEnd = -1;
|
|
1325
|
+
if (cursor < lines.length && indentOf(lines[cursor]) === indent) {
|
|
1326
|
+
if (/^\s*elif\b/.test(lines[cursor]))
|
|
1327
|
+
continue;
|
|
1328
|
+
if (/^\s*else\s*:\s*$/.test(lines[cursor])) {
|
|
1329
|
+
elseStart = cursor + 1;
|
|
1330
|
+
let k = elseStart;
|
|
1331
|
+
while (k < lines.length && (lines[k].trim() === '' || indentOf(lines[k]) > indent))
|
|
1332
|
+
k++;
|
|
1333
|
+
elseEnd = k - 1;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
if (decided === true) {
|
|
1337
|
+
for (let l = elseStart; l >= 0 && l <= elseEnd; l++)
|
|
1338
|
+
dead.add(l);
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
for (let l = thenStart; l <= thenEnd; l++)
|
|
1342
|
+
dead.add(l);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return dead;
|
|
1346
|
+
}
|
|
1168
1347
|
export function buildPythonTaintedVars(sourceCode) {
|
|
1169
1348
|
const tainted = new Map();
|
|
1170
1349
|
const containerTainted = new Map();
|
|
1171
1350
|
const lines = sourceCode.split('\n');
|
|
1351
|
+
const deadBranchLines = computeDeadPythonBranchLines(lines);
|
|
1352
|
+
// Per-element taint for lists followed since construction (`x = []` or a
|
|
1353
|
+
// literal). Only append / insert / pop / del are modelled; any other
|
|
1354
|
+
// mutation removes the list from the map and the coarse
|
|
1355
|
+
// whole-container behaviour takes over.
|
|
1356
|
+
const listElems = new Map();
|
|
1357
|
+
// Integer constants in scope, for folding `X = A if <const cond> else B`.
|
|
1358
|
+
// Only literal-valued and constant-derived integers are tracked; anything
|
|
1359
|
+
// else (call result, string, tainted read) removes the name from the map so
|
|
1360
|
+
// a later condition mentioning it stays undecided.
|
|
1361
|
+
const intConsts = new Map();
|
|
1172
1362
|
for (let i = 0; i < lines.length; i++) {
|
|
1173
1363
|
const line = lines[i];
|
|
1174
1364
|
if (line.trimStart().startsWith('#'))
|
|
1175
1365
|
continue;
|
|
1366
|
+
// A branch whose `if` condition is decidably false cannot assign anything,
|
|
1367
|
+
// so its lines must not seed or clobber taint.
|
|
1368
|
+
if (deadBranchLines.has(i))
|
|
1369
|
+
continue;
|
|
1176
1370
|
const subscriptAssign = line.match(/^\s*([\p{L}\p{N}_]+)\[(['"])([^'"]+)\2\]\s*=\s*(.+)$/u);
|
|
1177
1371
|
if (subscriptAssign) {
|
|
1178
1372
|
const [, container, , key, rhs2] = subscriptAssign;
|
|
@@ -1197,11 +1391,67 @@ export function buildPythonTaintedVars(sourceCode) {
|
|
|
1197
1391
|
// word-boundary scan below.
|
|
1198
1392
|
const containerAppendMatch = line.match(/^\s*([\p{L}\p{N}_]+)\.(append|extend|insert|add|push|put|appendleft)\s*\(\s*(.+?)\s*\)\s*$/u);
|
|
1199
1393
|
if (containerAppendMatch) {
|
|
1200
|
-
const [, receiver, , argExpr] = containerAppendMatch;
|
|
1394
|
+
const [, receiver, method, argExpr] = containerAppendMatch;
|
|
1201
1395
|
const argIsTainted = [...tainted.keys()].some(v => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, 'u').test(argExpr));
|
|
1202
1396
|
const argIsDirectSource = PYTHON_TAINTED_PATTERNS.some(p => p.pattern.test(argExpr));
|
|
1203
1397
|
if (argIsTainted || argIsDirectSource)
|
|
1204
1398
|
tainted.set(receiver, tainted.get(receiver) ?? (i + 1));
|
|
1399
|
+
// Per-element tracking for lists we have followed since construction.
|
|
1400
|
+
// `append` pushes; `insert(i, x)` splices at a literal index; anything
|
|
1401
|
+
// else (extend, an unparsable index) makes the element map unreliable,
|
|
1402
|
+
// so the list drops back to the coarse whole-container behaviour above.
|
|
1403
|
+
const elems = listElems.get(receiver);
|
|
1404
|
+
if (elems) {
|
|
1405
|
+
if (method === 'append') {
|
|
1406
|
+
elems.push(argIsTainted || argIsDirectSource);
|
|
1407
|
+
}
|
|
1408
|
+
else if (method === 'insert') {
|
|
1409
|
+
const insertAt = argExpr.match(/^(-?\d+)\s*,\s*(.+)$/);
|
|
1410
|
+
if (insertAt) {
|
|
1411
|
+
const idx = normalizeListIndex(Number(insertAt[1]), elems.length);
|
|
1412
|
+
const valueExpr = insertAt[2];
|
|
1413
|
+
const valueTainted = [...tainted.keys()].some(v => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, 'u').test(valueExpr)) ||
|
|
1414
|
+
PYTHON_TAINTED_PATTERNS.some(p => p.pattern.test(valueExpr));
|
|
1415
|
+
if (idx !== undefined)
|
|
1416
|
+
elems.splice(idx, 0, valueTainted);
|
|
1417
|
+
else
|
|
1418
|
+
listElems.delete(receiver);
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
listElems.delete(receiver);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
listElems.delete(receiver);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
// `lst.pop(i)` / `lst.pop()` / `del lst[i]` — removal shifts every later
|
|
1431
|
+
// element down one, which is exactly what makes `lst[1]` safe in
|
|
1432
|
+
// lst.append('safe'); lst.append(param); lst.append('moresafe')
|
|
1433
|
+
// lst.pop(0); bar = lst[1] # 'moresafe', not param
|
|
1434
|
+
const popMatch = line.match(/^\s*(?:([\p{L}\p{N}_]+)\s*=\s*)?([\p{L}\p{N}_]+)\.pop\s*\(\s*(-?\d+)?\s*\)\s*$/u);
|
|
1435
|
+
const delMatch = line.match(/^\s*del\s+([\p{L}\p{N}_]+)\s*\[\s*(-?\d+)\s*\]\s*$/u);
|
|
1436
|
+
if (popMatch || delMatch) {
|
|
1437
|
+
const receiver = popMatch ? popMatch[2] : delMatch[1];
|
|
1438
|
+
const rawIndex = popMatch ? (popMatch[3] !== undefined ? Number(popMatch[3]) : -1) : Number(delMatch[2]);
|
|
1439
|
+
const elems = listElems.get(receiver);
|
|
1440
|
+
if (elems) {
|
|
1441
|
+
const idx = normalizeListIndex(rawIndex, elems.length);
|
|
1442
|
+
if (idx === undefined)
|
|
1443
|
+
listElems.delete(receiver);
|
|
1444
|
+
else {
|
|
1445
|
+
const [removed] = elems.splice(idx, 1);
|
|
1446
|
+
// `x = lst.pop(i)` binds the removed element's taint to x.
|
|
1447
|
+
if (popMatch && popMatch[1]) {
|
|
1448
|
+
if (removed)
|
|
1449
|
+
tainted.set(popMatch[1], i + 1);
|
|
1450
|
+
else
|
|
1451
|
+
tainted.delete(popMatch[1]);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1205
1455
|
continue;
|
|
1206
1456
|
}
|
|
1207
1457
|
const augAssign = line.match(/^\s*([\p{L}\p{N}_]+)\s*\+=\s*(.+)$/u);
|
|
@@ -1224,7 +1474,70 @@ export function buildPythonTaintedVars(sourceCode) {
|
|
|
1224
1474
|
const assignMatch = line.match(/^\s*([\p{L}\p{N}_]+)\s*=\s*(.+)$/u);
|
|
1225
1475
|
if (!assignMatch)
|
|
1226
1476
|
continue;
|
|
1227
|
-
const [, lhs,
|
|
1477
|
+
const [, lhs, rhsRaw] = assignMatch;
|
|
1478
|
+
// Maintain the integer-constant map used to fold conditional expressions.
|
|
1479
|
+
const constValue = evaluatePythonConstExpression(rhsRaw.trim(), intConsts);
|
|
1480
|
+
if (typeof constValue === 'number')
|
|
1481
|
+
intConsts.set(lhs, constValue);
|
|
1482
|
+
else
|
|
1483
|
+
intConsts.delete(lhs);
|
|
1484
|
+
// Fold `X = <then> if <condition> else <else>` when the condition is
|
|
1485
|
+
// decidable from those constants. The dead branch cannot contribute taint,
|
|
1486
|
+
// so scanning the whole RHS for tainted names — as the generic path below
|
|
1487
|
+
// does — reports a flow that can never execute. This is the second-largest
|
|
1488
|
+
// FP shape on OWASP BenchmarkPython (`bar = "safe" if 7 * 18 + num > 200
|
|
1489
|
+
// else param`, where num is 106 and the safe literal always wins).
|
|
1490
|
+
//
|
|
1491
|
+
// Only the *condition* is folded, never the branches: `bar = a if c else b`
|
|
1492
|
+
// keeps whichever branch is live, taint and all. An undecidable condition
|
|
1493
|
+
// leaves the RHS untouched, so unknown values stay conservative.
|
|
1494
|
+
let rhs = rhsRaw;
|
|
1495
|
+
const ternary = rhsRaw.match(/^(.+?)\s+if\s+(.+?)\s+else\s+(.+)$/);
|
|
1496
|
+
if (ternary) {
|
|
1497
|
+
const decided = evaluatePythonConstExpression(ternary[2].trim(), intConsts);
|
|
1498
|
+
if (decided === true)
|
|
1499
|
+
rhs = ternary[1].trim();
|
|
1500
|
+
else if (decided === false)
|
|
1501
|
+
rhs = ternary[3].trim();
|
|
1502
|
+
}
|
|
1503
|
+
// Track list construction so the element map has a starting point, and
|
|
1504
|
+
// invalidate it whenever the name is rebound to something else.
|
|
1505
|
+
const listLiteral = rhs.trim().match(/^\[\s*(.*?)\s*\]$/);
|
|
1506
|
+
if (listLiteral) {
|
|
1507
|
+
const inner = listLiteral[1];
|
|
1508
|
+
if (inner === '') {
|
|
1509
|
+
listElems.set(lhs, []);
|
|
1510
|
+
}
|
|
1511
|
+
else if (!inner.includes('[') && !inner.includes('(')) {
|
|
1512
|
+
listElems.set(lhs, inner.split(',').map(part => {
|
|
1513
|
+
const expr = part.trim();
|
|
1514
|
+
return ([...tainted.keys()].some(v => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, 'u').test(expr)) ||
|
|
1515
|
+
PYTHON_TAINTED_PATTERNS.some(p => p.pattern.test(expr)));
|
|
1516
|
+
}));
|
|
1517
|
+
}
|
|
1518
|
+
else {
|
|
1519
|
+
listElems.delete(lhs);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
else {
|
|
1523
|
+
listElems.delete(lhs);
|
|
1524
|
+
}
|
|
1525
|
+
// Indexed read of a tracked list: `bar = lst[1]` takes that element's
|
|
1526
|
+
// taint precisely instead of inheriting the whole container's.
|
|
1527
|
+
const indexRead = rhs.trim().match(/^([\p{L}\p{N}_]+)\s*\[\s*(-?\d+)\s*\]$/u);
|
|
1528
|
+
if (indexRead) {
|
|
1529
|
+
const elems = listElems.get(indexRead[1]);
|
|
1530
|
+
if (elems) {
|
|
1531
|
+
const idx = normalizeListIndex(Number(indexRead[2]), elems.length);
|
|
1532
|
+
if (idx !== undefined) {
|
|
1533
|
+
if (elems[idx])
|
|
1534
|
+
tainted.set(lhs, i + 1);
|
|
1535
|
+
else
|
|
1536
|
+
tainted.delete(lhs);
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1228
1541
|
const isDirectSource = PYTHON_TAINTED_PATTERNS.some(p => p.pattern.test(rhs));
|
|
1229
1542
|
let propagatedFrom;
|
|
1230
1543
|
const dictAccessMatch = rhs.trim().match(/^([\p{L}\p{N}_]+)\[(['"])([^'"]+)\2\]$/u);
|
|
@@ -3055,6 +3368,214 @@ function findPythonTraversalRejectGuardSanitizers(code) {
|
|
|
3055
3368
|
}
|
|
3056
3369
|
return sanitizers;
|
|
3057
3370
|
}
|
|
3371
|
+
/**
|
|
3372
|
+
* Detect the Python "must be a plain string literal" validation guard
|
|
3373
|
+
* (cognium-dev #4 residual-FP audit):
|
|
3374
|
+
*
|
|
3375
|
+
* if not bar.startswith("'") or not bar.endswith("'") or "'" in bar[1:-1]:
|
|
3376
|
+
* return "Exec argument must be a plain string literal."
|
|
3377
|
+
* exec(bar)
|
|
3378
|
+
*
|
|
3379
|
+
* Past that guard `bar` is a quoted literal whose interior contains no quote,
|
|
3380
|
+
* so `exec`/`eval` sees a string-literal expression and cannot be steered into
|
|
3381
|
+
* executing attacker code — there is no way to close the quote and append a
|
|
3382
|
+
* statement. This is the whole codeinj FP bucket on OWASP BenchmarkPython.
|
|
3383
|
+
*
|
|
3384
|
+
* All three clauses are required. Any two of them leave an escape: without the
|
|
3385
|
+
* interior check, `'a' + evil() + '` satisfies startswith/endswith while still
|
|
3386
|
+
* breaking out of the literal. Only `code_injection` is sanitized — proving a
|
|
3387
|
+
* value is a quoted literal says nothing about SQL, a path, or HTML, where the
|
|
3388
|
+
* quote characters themselves are the payload.
|
|
3389
|
+
*/
|
|
3390
|
+
function findPythonStringLiteralGuardSanitizers(code) {
|
|
3391
|
+
const sanitizers = [];
|
|
3392
|
+
const lines = code.split('\n');
|
|
3393
|
+
const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
|
|
3394
|
+
// Quote literal in Python source: '\'' , "'" , '"' , "\"".
|
|
3395
|
+
const Q = `(?:'(?:\\\\')'|"'"|'"'|"(?:\\\\")")`;
|
|
3396
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3397
|
+
const guard = lines[i].match(new RegExp(`^(\\s*)if\\s+not\\s+([A-Za-z_][A-Za-z0-9_]*)\\.startswith\\(\\s*${Q}\\s*\\)` +
|
|
3398
|
+
`\\s+or\\s+not\\s+\\2\\.endswith\\(\\s*${Q}\\s*\\)` +
|
|
3399
|
+
`\\s+or\\s+${Q}\\s+in\\s+\\2\\[1:-1\\]\\s*:\\s*$`));
|
|
3400
|
+
if (!guard)
|
|
3401
|
+
continue;
|
|
3402
|
+
const guardIndent = guard[1].length;
|
|
3403
|
+
const guardedVar = guard[2];
|
|
3404
|
+
let bodyHasTerminator = false;
|
|
3405
|
+
let blockEnd = -1;
|
|
3406
|
+
const maxScan = Math.min(lines.length, i + 26);
|
|
3407
|
+
for (let j = i + 1; j < maxScan; j++) {
|
|
3408
|
+
const line = lines[j];
|
|
3409
|
+
if (line.trim() === '')
|
|
3410
|
+
continue;
|
|
3411
|
+
const indent = line.length - line.trimStart().length;
|
|
3412
|
+
if (indent <= guardIndent) {
|
|
3413
|
+
blockEnd = j - 1;
|
|
3414
|
+
break;
|
|
3415
|
+
}
|
|
3416
|
+
if (terminator.test(line))
|
|
3417
|
+
bodyHasTerminator = true;
|
|
3418
|
+
}
|
|
3419
|
+
if (blockEnd === -1)
|
|
3420
|
+
blockEnd = Math.min(lines.length - 1, i + 25);
|
|
3421
|
+
if (!bodyHasTerminator)
|
|
3422
|
+
continue;
|
|
3423
|
+
const varRe = new RegExp(`\\b${guardedVar}\\b`);
|
|
3424
|
+
for (let l = blockEnd + 2; l <= lines.length; l++) {
|
|
3425
|
+
if (!varRe.test(lines[l - 1]))
|
|
3426
|
+
continue;
|
|
3427
|
+
sanitizers.push({
|
|
3428
|
+
type: 'python_string_literal_guard',
|
|
3429
|
+
method: 'if',
|
|
3430
|
+
line: l,
|
|
3431
|
+
sanitizes: ['code_injection'],
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
return sanitizers;
|
|
3436
|
+
}
|
|
3437
|
+
/**
|
|
3438
|
+
* Detect a default-configured `xml.sax` parser being used to parse untrusted
|
|
3439
|
+
* XML (cognium-dev #4 residual-FP audit):
|
|
3440
|
+
*
|
|
3441
|
+
* parser = xml.sax.make_parser()
|
|
3442
|
+
* doc = xml.dom.minidom.parseString(bar, parser) # NOT XXE
|
|
3443
|
+
*
|
|
3444
|
+
* versus the vulnerable form, which differs by one line:
|
|
3445
|
+
*
|
|
3446
|
+
* parser = xml.sax.make_parser()
|
|
3447
|
+
* parser.setFeature(xml.sax.handler.feature_external_ges, True)
|
|
3448
|
+
* doc = xml.dom.minidom.parseString(bar, parser) # XXE
|
|
3449
|
+
*
|
|
3450
|
+
* Since Python 3.7.1 (bpo-17239) `xml.sax` and `xml.dom.minidom` do not
|
|
3451
|
+
* process external entities unless `feature_external_ges` is switched on or a
|
|
3452
|
+
* custom `EntityResolver` is installed. Without one of those, untrusted XML
|
|
3453
|
+
* cannot pull in an external entity, so CWE-611 does not apply. Residual DoS
|
|
3454
|
+
* risk (billion laughs / quadratic blowup) is a different weakness, CWE-776,
|
|
3455
|
+
* and is unaffected by this gate.
|
|
3456
|
+
*
|
|
3457
|
+
* The check is per-parser-variable and file-scoped: if the parser is enabled
|
|
3458
|
+
* anywhere, reassigned, or passed somewhere this scan cannot follow, no
|
|
3459
|
+
* sanitizer is emitted and the sink stands. In the OWASP BenchmarkPython
|
|
3460
|
+
* corpus this separates all 8 XXE true positives (which set the feature) from
|
|
3461
|
+
* the 3 false positives (which do not) — both groups call the same API.
|
|
3462
|
+
*/
|
|
3463
|
+
function findPythonDefaultSaxParserXxeSanitizers(code) {
|
|
3464
|
+
const sanitizers = [];
|
|
3465
|
+
const lines = code.split('\n');
|
|
3466
|
+
const parserVars = new Set();
|
|
3467
|
+
for (const line of lines) {
|
|
3468
|
+
const made = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:xml\.)?sax\.make_parser\s*\(/);
|
|
3469
|
+
if (made)
|
|
3470
|
+
parserVars.add(made[1]);
|
|
3471
|
+
}
|
|
3472
|
+
if (parserVars.size === 0)
|
|
3473
|
+
return sanitizers;
|
|
3474
|
+
// Drop any parser that has entity processing switched on, gets a custom
|
|
3475
|
+
// resolver, or is rebound to something else.
|
|
3476
|
+
for (const line of lines) {
|
|
3477
|
+
for (const parser of [...parserVars]) {
|
|
3478
|
+
const enabled = new RegExp(`\\b${parser}\\.setFeature\\s*\\([^,]*external_(?:ges|pes)[^,]*,\\s*True\\s*\\)`);
|
|
3479
|
+
const resolver = new RegExp(`\\b${parser}\\.setEntityResolver\\s*\\(`);
|
|
3480
|
+
const rebound = new RegExp(`^\\s*${parser}\\s*=(?!=)`);
|
|
3481
|
+
if (enabled.test(line) || resolver.test(line))
|
|
3482
|
+
parserVars.delete(parser);
|
|
3483
|
+
else if (rebound.test(line) && !/make_parser\s*\(/.test(line))
|
|
3484
|
+
parserVars.delete(parser);
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
if (parserVars.size === 0)
|
|
3488
|
+
return sanitizers;
|
|
3489
|
+
// Emit on parse calls that hand the XML to one of those parsers.
|
|
3490
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3491
|
+
const line = lines[i];
|
|
3492
|
+
const parseCall = /\b(?:minidom\.)?parseString\s*\(|\b(?:minidom\.)?parse\s*\(|\bsax\.parse(?:String)?\s*\(/;
|
|
3493
|
+
if (!parseCall.test(line))
|
|
3494
|
+
continue;
|
|
3495
|
+
const usesDefaultParser = [...parserVars].some(p => new RegExp(`\\b${p}\\b`).test(line));
|
|
3496
|
+
if (!usesDefaultParser)
|
|
3497
|
+
continue;
|
|
3498
|
+
sanitizers.push({
|
|
3499
|
+
type: 'python_default_sax_parser',
|
|
3500
|
+
method: 'make_parser',
|
|
3501
|
+
line: i + 1,
|
|
3502
|
+
sanitizes: ['xxe'],
|
|
3503
|
+
});
|
|
3504
|
+
}
|
|
3505
|
+
return sanitizers;
|
|
3506
|
+
}
|
|
3507
|
+
/**
|
|
3508
|
+
* Detect the parse-and-allowlist open-redirect guard (cognium-dev #4
|
|
3509
|
+
* residual-FP audit):
|
|
3510
|
+
*
|
|
3511
|
+
* url = urllib.parse.urlparse(bar)
|
|
3512
|
+
* if url.netloc not in ['google.com'] or url.scheme != 'https':
|
|
3513
|
+
* return "Invalid URL."
|
|
3514
|
+
* return flask.redirect(bar)
|
|
3515
|
+
*
|
|
3516
|
+
* Past the guard the target's host is one of a fixed set, which is the
|
|
3517
|
+
* canonical CWE-601 mitigation — an attacker cannot steer the redirect
|
|
3518
|
+
* off-site. Requires the reject polarity with a terminator, a `netloc`
|
|
3519
|
+
* membership test against a literal collection, and a `urlparse` whose
|
|
3520
|
+
* argument is the value later redirected to.
|
|
3521
|
+
*
|
|
3522
|
+
* Only `open_redirect` is sanitized: knowing the host is allow-listed says
|
|
3523
|
+
* nothing about the path or query, so SSRF and injection sinks are untouched.
|
|
3524
|
+
*/
|
|
3525
|
+
function findPythonUrlAllowlistRedirectSanitizers(code) {
|
|
3526
|
+
const sanitizers = [];
|
|
3527
|
+
const lines = code.split('\n');
|
|
3528
|
+
// `url = urlparse(bar)` → parsed name maps back to the raw URL variable.
|
|
3529
|
+
const parsedFrom = new Map();
|
|
3530
|
+
for (const line of lines) {
|
|
3531
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:urllib\.parse\.)?urlparse\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/);
|
|
3532
|
+
if (m)
|
|
3533
|
+
parsedFrom.set(m[1], m[2]);
|
|
3534
|
+
}
|
|
3535
|
+
if (parsedFrom.size === 0)
|
|
3536
|
+
return sanitizers;
|
|
3537
|
+
const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
|
|
3538
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3539
|
+
const guard = lines[i].match(/^(\s*)if\s+([A-Za-z_][A-Za-z0-9_]*)\.netloc\s+not\s+in\s+[[({]/);
|
|
3540
|
+
if (!guard)
|
|
3541
|
+
continue;
|
|
3542
|
+
const guardIndent = guard[1].length;
|
|
3543
|
+
const rawVar = parsedFrom.get(guard[2]);
|
|
3544
|
+
if (!rawVar)
|
|
3545
|
+
continue;
|
|
3546
|
+
let bodyHasTerminator = false;
|
|
3547
|
+
let blockEnd = -1;
|
|
3548
|
+
const maxScan = Math.min(lines.length, i + 26);
|
|
3549
|
+
for (let j = i + 1; j < maxScan; j++) {
|
|
3550
|
+
const line = lines[j];
|
|
3551
|
+
if (line.trim() === '')
|
|
3552
|
+
continue;
|
|
3553
|
+
const indent = line.length - line.trimStart().length;
|
|
3554
|
+
if (indent <= guardIndent) {
|
|
3555
|
+
blockEnd = j - 1;
|
|
3556
|
+
break;
|
|
3557
|
+
}
|
|
3558
|
+
if (terminator.test(line))
|
|
3559
|
+
bodyHasTerminator = true;
|
|
3560
|
+
}
|
|
3561
|
+
if (blockEnd === -1)
|
|
3562
|
+
blockEnd = Math.min(lines.length - 1, i + 25);
|
|
3563
|
+
if (!bodyHasTerminator)
|
|
3564
|
+
continue;
|
|
3565
|
+
const varRe = new RegExp(`\\b${rawVar}\\b`);
|
|
3566
|
+
for (let l = blockEnd + 2; l <= lines.length; l++) {
|
|
3567
|
+
if (!varRe.test(lines[l - 1]))
|
|
3568
|
+
continue;
|
|
3569
|
+
sanitizers.push({
|
|
3570
|
+
type: 'python_url_allowlist_guard',
|
|
3571
|
+
method: 'if',
|
|
3572
|
+
line: l,
|
|
3573
|
+
sanitizes: ['open_redirect'],
|
|
3574
|
+
});
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
return sanitizers;
|
|
3578
|
+
}
|
|
3058
3579
|
/**
|
|
3059
3580
|
* Detect Python `defusedxml` import-alias usage as xxe sanitizer
|
|
3060
3581
|
* (cognium-dev #216 Sprint 74 Pattern B).
|