circle-ir 3.58.0 → 3.62.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/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +58 -17
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/html/html-merge.d.ts.map +1 -1
- package/dist/analysis/html/html-merge.js +10 -0
- package/dist/analysis/html/html-merge.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +44 -11
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +7 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +283 -15
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/missing-public-doc-pass.d.ts.map +1 -1
- package/dist/analysis/passes/missing-public-doc-pass.js +2 -1
- package/dist/analysis/passes/missing-public-doc-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 +4 -1
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +2 -1
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-random-pass.js +2 -1
- package/dist/analysis/passes/weak-random-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +29 -7
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +20 -0
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +19 -2
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +512 -51
- package/dist/core/circle-ir-core.cjs +243 -26
- package/dist/core/circle-ir-core.js +243 -26
- package/dist/core/extractors/calls.js +181 -1
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/core/extractors/cfg.js +1 -1
- package/dist/core/extractors/cfg.js.map +1 -1
- package/dist/core/extractors/dfg.js +29 -3
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/core/extractors/imports.js +1 -1
- package/dist/core/extractors/imports.js.map +1 -1
- package/dist/core/extractors/runtime-registrations.js +1 -1
- package/dist/core/extractors/runtime-registrations.js.map +1 -1
- package/dist/core/extractors/types.js +1 -1
- package/dist/core/extractors/types.js.map +1 -1
- package/dist/core/parser.d.ts +1 -1
- package/dist/core/parser.d.ts.map +1 -1
- package/dist/graph/scope-graph.d.ts.map +1 -1
- package/dist/graph/scope-graph.js +1 -0
- package/dist/graph/scope-graph.js.map +1 -1
- package/dist/languages/plugins/bash.d.ts.map +1 -1
- package/dist/languages/plugins/bash.js +17 -0
- package/dist/languages/plugins/bash.js.map +1 -1
- package/dist/languages/registry.d.ts.map +1 -1
- package/dist/languages/registry.js +6 -0
- package/dist/languages/registry.js.map +1 -1
- package/dist/languages/types.d.ts +1 -1
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/wasm/tree-sitter-tsx.wasm +0 -0
- package/package.json +2 -1
|
@@ -4321,7 +4321,7 @@ function detectLanguage(tree) {
|
|
|
4321
4321
|
}
|
|
4322
4322
|
function extractTypes(tree, cache, language) {
|
|
4323
4323
|
const effectiveLanguage = language ?? detectLanguage(tree);
|
|
4324
|
-
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
|
|
4324
|
+
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
|
|
4325
4325
|
const isPython = effectiveLanguage === "python";
|
|
4326
4326
|
const isRust = effectiveLanguage === "rust";
|
|
4327
4327
|
if (effectiveLanguage === "go") {
|
|
@@ -5707,7 +5707,7 @@ function detectLanguageFromTree(tree, cache) {
|
|
|
5707
5707
|
function extractCalls(tree, cache, language) {
|
|
5708
5708
|
const calls = [];
|
|
5709
5709
|
const detectedLanguage = language ?? detectLanguageFromTree(tree, cache);
|
|
5710
|
-
const isJavaScript = detectedLanguage === "javascript" || detectedLanguage === "typescript";
|
|
5710
|
+
const isJavaScript = detectedLanguage === "javascript" || detectedLanguage === "typescript" || detectedLanguage === "tsx";
|
|
5711
5711
|
const isPython = detectedLanguage === "python";
|
|
5712
5712
|
const isRust = detectedLanguage === "rust";
|
|
5713
5713
|
if (detectedLanguage === "go") {
|
|
@@ -5756,8 +5756,137 @@ function extractJavaScriptCalls(tree, cache) {
|
|
|
5756
5756
|
calls.push(callInfo);
|
|
5757
5757
|
}
|
|
5758
5758
|
}
|
|
5759
|
+
const jsxAttributes = getNodesFromCache(tree.rootNode, "jsx_attribute", cache);
|
|
5760
|
+
for (const attr of jsxAttributes) {
|
|
5761
|
+
const callInfo = extractJSXAttributeSink(attr);
|
|
5762
|
+
if (callInfo) {
|
|
5763
|
+
calls.push(callInfo);
|
|
5764
|
+
}
|
|
5765
|
+
}
|
|
5766
|
+
const assignments = getNodesFromCache(tree.rootNode, "assignment_expression", cache);
|
|
5767
|
+
for (const assign of assignments) {
|
|
5768
|
+
const callInfo = extractDomPropertyAssignmentSink(assign);
|
|
5769
|
+
if (callInfo) {
|
|
5770
|
+
calls.push(callInfo);
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5759
5773
|
return calls;
|
|
5760
5774
|
}
|
|
5775
|
+
var DOM_XSS_ASSIGNMENT_PROPERTIES = /* @__PURE__ */ new Set([
|
|
5776
|
+
"innerHTML",
|
|
5777
|
+
"outerHTML"
|
|
5778
|
+
]);
|
|
5779
|
+
function extractDomPropertyAssignmentSink(node) {
|
|
5780
|
+
const leftNode = node.childForFieldName("left");
|
|
5781
|
+
const rightNode = node.childForFieldName("right");
|
|
5782
|
+
if (!leftNode || !rightNode) return null;
|
|
5783
|
+
if (leftNode.type !== "member_expression") return null;
|
|
5784
|
+
const propertyNode = leftNode.childForFieldName("property");
|
|
5785
|
+
const objectNode = leftNode.childForFieldName("object");
|
|
5786
|
+
if (!propertyNode) return null;
|
|
5787
|
+
const propertyName = getNodeText(propertyNode);
|
|
5788
|
+
if (!DOM_XSS_ASSIGNMENT_PROPERTIES.has(propertyName)) return null;
|
|
5789
|
+
const receiver = objectNode ? getNodeText(objectNode) : null;
|
|
5790
|
+
const expression = getNodeText(rightNode);
|
|
5791
|
+
const { variable, literal } = analyzeJSArgument(rightNode);
|
|
5792
|
+
const enclosingFunc = findJSEnclosingFunction(node);
|
|
5793
|
+
return {
|
|
5794
|
+
method_name: propertyName,
|
|
5795
|
+
receiver,
|
|
5796
|
+
arguments: [
|
|
5797
|
+
{
|
|
5798
|
+
position: 0,
|
|
5799
|
+
expression,
|
|
5800
|
+
variable,
|
|
5801
|
+
literal
|
|
5802
|
+
}
|
|
5803
|
+
],
|
|
5804
|
+
location: {
|
|
5805
|
+
line: node.startPosition.row + 1,
|
|
5806
|
+
column: node.startPosition.column
|
|
5807
|
+
},
|
|
5808
|
+
in_method: enclosingFunc,
|
|
5809
|
+
resolved: true,
|
|
5810
|
+
resolution: {
|
|
5811
|
+
status: "resolved",
|
|
5812
|
+
target: `DOM.${propertyName}`
|
|
5813
|
+
}
|
|
5814
|
+
};
|
|
5815
|
+
}
|
|
5816
|
+
function extractJSXAttributeSink(attr) {
|
|
5817
|
+
let nameNode = null;
|
|
5818
|
+
for (let i2 = 0; i2 < attr.childCount; i2++) {
|
|
5819
|
+
const child = attr.child(i2);
|
|
5820
|
+
if (child && child.type === "property_identifier") {
|
|
5821
|
+
nameNode = child;
|
|
5822
|
+
break;
|
|
5823
|
+
}
|
|
5824
|
+
}
|
|
5825
|
+
if (!nameNode) return null;
|
|
5826
|
+
const attrName = getNodeText(nameNode);
|
|
5827
|
+
if (attrName !== "dangerouslySetInnerHTML") return null;
|
|
5828
|
+
let valueExpr = null;
|
|
5829
|
+
for (let i2 = 0; i2 < attr.childCount; i2++) {
|
|
5830
|
+
const child = attr.child(i2);
|
|
5831
|
+
if (child && child.type === "jsx_expression") {
|
|
5832
|
+
valueExpr = child;
|
|
5833
|
+
break;
|
|
5834
|
+
}
|
|
5835
|
+
}
|
|
5836
|
+
if (!valueExpr) return null;
|
|
5837
|
+
let htmlValue = null;
|
|
5838
|
+
for (let i2 = 0; i2 < valueExpr.childCount; i2++) {
|
|
5839
|
+
const inner = valueExpr.child(i2);
|
|
5840
|
+
if (!inner || inner.type !== "object") continue;
|
|
5841
|
+
for (let j = 0; j < inner.childCount; j++) {
|
|
5842
|
+
const pair = inner.child(j);
|
|
5843
|
+
if (!pair || pair.type !== "pair") continue;
|
|
5844
|
+
const keyNode = pair.childForFieldName("key");
|
|
5845
|
+
if (!keyNode) continue;
|
|
5846
|
+
const keyText = getNodeText(keyNode).replace(/^["']|["']$/g, "");
|
|
5847
|
+
if (keyText === "__html") {
|
|
5848
|
+
htmlValue = pair.childForFieldName("value");
|
|
5849
|
+
break;
|
|
5850
|
+
}
|
|
5851
|
+
}
|
|
5852
|
+
if (htmlValue) break;
|
|
5853
|
+
}
|
|
5854
|
+
if (!htmlValue) {
|
|
5855
|
+
for (let i2 = 0; i2 < valueExpr.childCount; i2++) {
|
|
5856
|
+
const inner = valueExpr.child(i2);
|
|
5857
|
+
if (inner && inner.type !== "{" && inner.type !== "}") {
|
|
5858
|
+
htmlValue = inner;
|
|
5859
|
+
break;
|
|
5860
|
+
}
|
|
5861
|
+
}
|
|
5862
|
+
}
|
|
5863
|
+
if (!htmlValue) return null;
|
|
5864
|
+
const expression = getNodeText(htmlValue);
|
|
5865
|
+
const { variable, literal } = analyzeJSArgument(htmlValue);
|
|
5866
|
+
const enclosingFunc = findJSEnclosingFunction(attr);
|
|
5867
|
+
return {
|
|
5868
|
+
method_name: "dangerouslySetInnerHTML",
|
|
5869
|
+
receiver: null,
|
|
5870
|
+
arguments: [
|
|
5871
|
+
{
|
|
5872
|
+
position: 0,
|
|
5873
|
+
expression,
|
|
5874
|
+
variable,
|
|
5875
|
+
literal
|
|
5876
|
+
}
|
|
5877
|
+
],
|
|
5878
|
+
location: {
|
|
5879
|
+
line: attr.startPosition.row + 1,
|
|
5880
|
+
column: attr.startPosition.column
|
|
5881
|
+
},
|
|
5882
|
+
in_method: enclosingFunc,
|
|
5883
|
+
resolved: true,
|
|
5884
|
+
resolution: {
|
|
5885
|
+
status: "resolved",
|
|
5886
|
+
target: "react.dangerouslySetInnerHTML"
|
|
5887
|
+
}
|
|
5888
|
+
};
|
|
5889
|
+
}
|
|
5761
5890
|
function buildJSResolutionContext(tree, cache) {
|
|
5762
5891
|
const context = {
|
|
5763
5892
|
functionNames: /* @__PURE__ */ new Set(),
|
|
@@ -7265,7 +7394,7 @@ function detectLanguage2(tree) {
|
|
|
7265
7394
|
}
|
|
7266
7395
|
function extractImports(tree, language) {
|
|
7267
7396
|
const effectiveLanguage = language ?? detectLanguage2(tree);
|
|
7268
|
-
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
|
|
7397
|
+
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
|
|
7269
7398
|
const isPython = effectiveLanguage === "python";
|
|
7270
7399
|
const isRust = effectiveLanguage === "rust";
|
|
7271
7400
|
if (effectiveLanguage === "go") {
|
|
@@ -7957,7 +8086,7 @@ function detectLanguage3(tree) {
|
|
|
7957
8086
|
}
|
|
7958
8087
|
function buildCFG(tree, language) {
|
|
7959
8088
|
const effectiveLanguage = language ?? detectLanguage3(tree);
|
|
7960
|
-
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
|
|
8089
|
+
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
|
|
7961
8090
|
const allBlocks = [];
|
|
7962
8091
|
const allEdges = [];
|
|
7963
8092
|
let blockIdCounter = 0;
|
|
@@ -8532,7 +8661,7 @@ function detectLanguage4(tree) {
|
|
|
8532
8661
|
}
|
|
8533
8662
|
function buildDFG(tree, cache, language) {
|
|
8534
8663
|
const effectiveLanguage = language ?? detectLanguage4(tree);
|
|
8535
|
-
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
|
|
8664
|
+
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
|
|
8536
8665
|
if (isJavaScript) {
|
|
8537
8666
|
return buildJavaScriptDFG(tree, cache);
|
|
8538
8667
|
}
|
|
@@ -9340,7 +9469,18 @@ function buildBashDFG(tree) {
|
|
|
9340
9469
|
if (varNameNode) {
|
|
9341
9470
|
const varName = getNodeText(varNameNode);
|
|
9342
9471
|
if (varName && !varName.startsWith("?") && !varName.startsWith("#")) {
|
|
9343
|
-
|
|
9472
|
+
let reachingDef = findReachingDef(varName, scopeStack);
|
|
9473
|
+
if (reachingDef === null && !positionalParams.includes(varName)) {
|
|
9474
|
+
const def = {
|
|
9475
|
+
id: defIdCounter++,
|
|
9476
|
+
variable: varName,
|
|
9477
|
+
line: 0,
|
|
9478
|
+
kind: "param"
|
|
9479
|
+
};
|
|
9480
|
+
defs.push(def);
|
|
9481
|
+
scopeStack[0].set(varName, def.id);
|
|
9482
|
+
reachingDef = def.id;
|
|
9483
|
+
}
|
|
9344
9484
|
uses.push({
|
|
9345
9485
|
id: useIdCounter++,
|
|
9346
9486
|
variable: varName,
|
|
@@ -9353,7 +9493,18 @@ function buildBashDFG(tree) {
|
|
|
9353
9493
|
const varNameNode = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
9354
9494
|
if (varNameNode && varNameNode.type === "variable_name") {
|
|
9355
9495
|
const varName = getNodeText(varNameNode);
|
|
9356
|
-
|
|
9496
|
+
let reachingDef = findReachingDef(varName, scopeStack);
|
|
9497
|
+
if (reachingDef === null && !positionalParams.includes(varName)) {
|
|
9498
|
+
const def = {
|
|
9499
|
+
id: defIdCounter++,
|
|
9500
|
+
variable: varName,
|
|
9501
|
+
line: 0,
|
|
9502
|
+
kind: "param"
|
|
9503
|
+
};
|
|
9504
|
+
defs.push(def);
|
|
9505
|
+
scopeStack[0].set(varName, def.id);
|
|
9506
|
+
reachingDef = def.id;
|
|
9507
|
+
}
|
|
9357
9508
|
uses.push({
|
|
9358
9509
|
id: useIdCounter++,
|
|
9359
9510
|
variable: varName,
|
|
@@ -10102,8 +10253,12 @@ var DEFAULT_SOURCES = [
|
|
|
10102
10253
|
{ method: "get", class: "cookies", type: "http_cookie", severity: "high", return_tainted: true },
|
|
10103
10254
|
{ property: "json", object: "request", type: "http_body", severity: "high", property_tainted: true },
|
|
10104
10255
|
{ property: "data", object: "request", type: "http_body", severity: "high", property_tainted: true },
|
|
10256
|
+
{ property: "stream", object: "request", type: "http_body", severity: "high", property_tainted: true },
|
|
10105
10257
|
{ property: "path", object: "request", type: "http_path", severity: "medium", property_tainted: true },
|
|
10106
10258
|
{ property: "query_string", object: "request", type: "http_query", severity: "high", property_tainted: true },
|
|
10259
|
+
// Flask request.get_data() — raw request bytes (method form, parallel to request.data property)
|
|
10260
|
+
{ method: "get_data", class: "request", type: "http_body", severity: "high", return_tainted: true },
|
|
10261
|
+
{ method: "get_json", class: "request", type: "http_body", severity: "high", return_tainted: true },
|
|
10107
10262
|
// Django request object
|
|
10108
10263
|
{ method: "get", class: "GET", type: "http_param", severity: "high", return_tainted: true },
|
|
10109
10264
|
{ method: "get", class: "POST", type: "http_param", severity: "high", return_tainted: true },
|
|
@@ -10315,14 +10470,19 @@ var DEFAULT_SINKS = [
|
|
|
10315
10470
|
{ method: "setExecutable", class: "ExecTask", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10316
10471
|
{ method: "setCommand", class: "ExecTask", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10317
10472
|
{ method: "execute", class: "Java", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10318
|
-
// Shell/Bash utilities
|
|
10319
|
-
|
|
10320
|
-
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
{
|
|
10324
|
-
{ method: "
|
|
10325
|
-
{ method: "
|
|
10473
|
+
// Shell/Bash utilities — these are method-call sinks in host languages
|
|
10474
|
+
// (Java Runtime/ProcessBuilder, JS child_process spawn/exec, Python subprocess, etc.).
|
|
10475
|
+
// When the analyzed file IS a bash/shell script, the bash plugin's per-flag entries
|
|
10476
|
+
// (argPositions: [1] for `bash -c <cmd>`) MUST win. Restrict these generic entries
|
|
10477
|
+
// to non-shell languages so they don't collide on the dedup key
|
|
10478
|
+
// `${location}:${call.location.line}:${pattern.cwe}`.
|
|
10479
|
+
{ method: "bash", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10480
|
+
{ method: "shell", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10481
|
+
{ method: "sh", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10482
|
+
{ method: "spawn", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10483
|
+
{ method: "fork", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10484
|
+
{ method: "popen", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10485
|
+
{ method: "system", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10326
10486
|
// Apache Commons Exec
|
|
10327
10487
|
// Note: bare class 'Executor' removed (see comment above) — DefaultExecutor matched explicitly.
|
|
10328
10488
|
{ method: "setCommandline", class: "DefaultExecutor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
@@ -10412,6 +10572,12 @@ var DEFAULT_SINKS = [
|
|
|
10412
10572
|
{ method: "unzip", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
|
|
10413
10573
|
{ method: "extract", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
|
|
10414
10574
|
{ method: "extractAll", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
|
|
10575
|
+
// Python zipfile/tarfile use lowercase extractall (PEP 8 naming)
|
|
10576
|
+
{ method: "extractall", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0], languages: ["python"] },
|
|
10577
|
+
// Python zipfile.ZipFile(path) — tainted archive path enables Zip-Slip via malicious archive
|
|
10578
|
+
{ method: "ZipFile", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
|
|
10579
|
+
// Flask send_from_directory: untrusted filename can escape directory via ../
|
|
10580
|
+
{ method: "send_from_directory", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [1], languages: ["python"] },
|
|
10415
10581
|
{ method: "unjar", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
|
|
10416
10582
|
// Additional file constructors — BufferedReader(Reader) is NOT a path traversal sink; it wraps a Reader, not a file path
|
|
10417
10583
|
{ method: "PrintWriter", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
@@ -11496,16 +11662,42 @@ var DEFAULT_SINKS = [
|
|
|
11496
11662
|
// value position so a tainted variable is detected.
|
|
11497
11663
|
{ method: "Set", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
|
|
11498
11664
|
{ method: "Add", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
|
|
11499
|
-
// Mass-assignment (CWE-915) — Sprint 6, #86.
|
|
11500
|
-
// JS Object.assign(target, ...sources)
|
|
11501
|
-
//
|
|
11502
|
-
//
|
|
11503
|
-
|
|
11504
|
-
//
|
|
11505
|
-
|
|
11506
|
-
|
|
11665
|
+
// Mass-assignment (CWE-915 / CWE-1321) — Sprint 6, #86; cognium-dev #68 Sprint 10.
|
|
11666
|
+
// JS Object.assign(target, ...sources), `_.merge`, `_.extend`, `$.extend`,
|
|
11667
|
+
// `Object.defineProperty` — when fed an attacker-controlled bag, they write
|
|
11668
|
+
// arbitrary keys onto the target (or, for `__proto__`/`constructor.prototype`,
|
|
11669
|
+
// pollute the prototype chain). The CWE is CWE-1321 (Prototype Pollution),
|
|
11670
|
+
// which subsumes mass assignment for JS sinks operating on plain Objects.
|
|
11671
|
+
// We keep the existing `mass_assignment` SinkType so consumers route the
|
|
11672
|
+
// findings the same way; only the CWE shifts to flag prototype-pollution.
|
|
11673
|
+
{ method: "assign", class: "Object", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11674
|
+
{ method: "defineProperty", class: "Object", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2], languages: ["javascript", "typescript"] },
|
|
11675
|
+
{ method: "defineProperties", class: "Object", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1], languages: ["javascript", "typescript"] },
|
|
11676
|
+
// Lodash bulk-merge helpers behave identically. `_.merge` and `lodash.merge`
|
|
11677
|
+
// are aliases — match both receivers.
|
|
11678
|
+
{ method: "merge", class: "_", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11679
|
+
{ method: "merge", class: "lodash", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11680
|
+
{ method: "extend", class: "_", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11681
|
+
{ method: "extend", class: "lodash", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11682
|
+
{ method: "defaultsDeep", class: "_", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11683
|
+
{ method: "defaultsDeep", class: "lodash", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11507
11684
|
// jQuery $.extend(target, source) (legacy).
|
|
11508
|
-
{ method: "extend", class: "$", type: "mass_assignment", cwe: "CWE-
|
|
11685
|
+
{ method: "extend", class: "$", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11686
|
+
{ method: "extend", class: "jQuery", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11687
|
+
// DOM-XSS via property assignment (CWE-79) — cognium-dev #68 Sprint 10.
|
|
11688
|
+
// `el.innerHTML = tainted` / `el.outerHTML = tainted`. The JS call extractor
|
|
11689
|
+
// emits a synthetic CallInfo with method=`innerHTML`/`outerHTML` for each
|
|
11690
|
+
// matching assignment_expression. These classless entries catch them.
|
|
11691
|
+
{ method: "innerHTML", type: "xss", cwe: "CWE-79", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11692
|
+
{ method: "outerHTML", type: "xss", cwe: "CWE-79", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11693
|
+
// node-serialize.unserialize (CWE-502) — cognium-dev #68 Sprint 10.
|
|
11694
|
+
// The node-serialize package evaluates `_$$ND_FUNC$$_` IIFE payloads on
|
|
11695
|
+
// decode, turning untrusted input into RCE. Match both receiver-bound
|
|
11696
|
+
// calls (`serialize.unserialize(x)`) and destructured imports
|
|
11697
|
+
// (`const { unserialize } = require('node-serialize')`).
|
|
11698
|
+
{ method: "unserialize", class: "serialize", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11699
|
+
{ method: "unserialize", class: "node-serialize", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11700
|
+
{ method: "unserialize", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] }
|
|
11509
11701
|
];
|
|
11510
11702
|
var DEFAULT_SANITIZERS = [
|
|
11511
11703
|
// SQL Injection - proper parameter binding sanitizes input
|
|
@@ -12112,7 +12304,12 @@ function matchesSourcePattern(call, pattern) {
|
|
|
12112
12304
|
if (call.receiver_type && call.receiver_type === pattern.class) {
|
|
12113
12305
|
} else if (call.receiver_type_fqn && call.receiver_type_fqn.endsWith("." + pattern.class)) {
|
|
12114
12306
|
} else if (!call.receiver) {
|
|
12115
|
-
|
|
12307
|
+
const target = call.resolution?.target;
|
|
12308
|
+
const expectedTail = `${pattern.class}.${pattern.method}`;
|
|
12309
|
+
if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
|
|
12310
|
+
} else {
|
|
12311
|
+
return false;
|
|
12312
|
+
}
|
|
12116
12313
|
} else if (!receiverMightBeClass(call.receiver, pattern.class)) {
|
|
12117
12314
|
return false;
|
|
12118
12315
|
}
|
|
@@ -12379,7 +12576,12 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
|
|
|
12379
12576
|
}
|
|
12380
12577
|
return false;
|
|
12381
12578
|
} else if (!call.receiver && !call.receiver_type) {
|
|
12382
|
-
|
|
12579
|
+
const target = call.resolution?.target;
|
|
12580
|
+
const expectedTail = `${pattern.class}.${pattern.method}`;
|
|
12581
|
+
if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
|
|
12582
|
+
} else {
|
|
12583
|
+
return false;
|
|
12584
|
+
}
|
|
12383
12585
|
}
|
|
12384
12586
|
}
|
|
12385
12587
|
if (!pattern.class && call.receiver) {
|
|
@@ -13221,6 +13423,21 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
|
|
|
13221
13423
|
});
|
|
13222
13424
|
}
|
|
13223
13425
|
}
|
|
13426
|
+
if (source.variable) {
|
|
13427
|
+
const paramDefs = defsByLine.get(0) ?? [];
|
|
13428
|
+
for (const def of paramDefs) {
|
|
13429
|
+
if (def.kind === "param" && def.variable === source.variable) {
|
|
13430
|
+
tainted.push({
|
|
13431
|
+
variable: def.variable,
|
|
13432
|
+
defId: def.id,
|
|
13433
|
+
line: def.line,
|
|
13434
|
+
sourceType: source.type,
|
|
13435
|
+
sourceLine: source.line,
|
|
13436
|
+
confidence: source.confidence
|
|
13437
|
+
});
|
|
13438
|
+
}
|
|
13439
|
+
}
|
|
13440
|
+
}
|
|
13224
13441
|
}
|
|
13225
13442
|
return tainted;
|
|
13226
13443
|
}
|
|
@@ -57,7 +57,7 @@ export function extractCalls(tree, cache, language) {
|
|
|
57
57
|
const calls = [];
|
|
58
58
|
// Use language hint if provided, otherwise detect from tree
|
|
59
59
|
const detectedLanguage = language ?? detectLanguageFromTree(tree, cache);
|
|
60
|
-
const isJavaScript = detectedLanguage === 'javascript' || detectedLanguage === 'typescript';
|
|
60
|
+
const isJavaScript = detectedLanguage === 'javascript' || detectedLanguage === 'typescript' || detectedLanguage === 'tsx';
|
|
61
61
|
const isPython = detectedLanguage === 'python';
|
|
62
62
|
const isRust = detectedLanguage === 'rust';
|
|
63
63
|
if (detectedLanguage === 'go') {
|
|
@@ -115,8 +115,188 @@ function extractJavaScriptCalls(tree, cache) {
|
|
|
115
115
|
calls.push(callInfo);
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
// Find JSX attributes that act as XSS sinks (e.g. dangerouslySetInnerHTML).
|
|
119
|
+
// The TSX/JSX grammar represents these as `jsx_attribute` nodes, not
|
|
120
|
+
// `call_expression`. To let the taint matcher reuse the standard method-
|
|
121
|
+
// call sink path, we emit a synthetic CallInfo per matching attribute.
|
|
122
|
+
// (cognium-dev #68 — Phase D.1)
|
|
123
|
+
const jsxAttributes = getNodesFromCache(tree.rootNode, 'jsx_attribute', cache);
|
|
124
|
+
for (const attr of jsxAttributes) {
|
|
125
|
+
const callInfo = extractJSXAttributeSink(attr);
|
|
126
|
+
if (callInfo) {
|
|
127
|
+
calls.push(callInfo);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Find DOM property assignments that act as XSS sinks
|
|
131
|
+
// (e.g. `el.innerHTML = userInput`). Same rationale as JSX attributes:
|
|
132
|
+
// emit a synthetic CallInfo so the standard taint matcher path picks them
|
|
133
|
+
// up via property-named sink entries. (cognium-dev #68 — Phase D.3)
|
|
134
|
+
const assignments = getNodesFromCache(tree.rootNode, 'assignment_expression', cache);
|
|
135
|
+
for (const assign of assignments) {
|
|
136
|
+
const callInfo = extractDomPropertyAssignmentSink(assign);
|
|
137
|
+
if (callInfo) {
|
|
138
|
+
calls.push(callInfo);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
118
141
|
return calls;
|
|
119
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* DOM properties whose assignment is an XSS sink. Keeping this list small
|
|
145
|
+
* and explicit so we don't over-flag (the YAML config in
|
|
146
|
+
* `configs/sinks/javascript_dom_xss.yaml` lists more, but most of those are
|
|
147
|
+
* element-conditional and would need DOM-type tracking to flag without FPs).
|
|
148
|
+
*/
|
|
149
|
+
const DOM_XSS_ASSIGNMENT_PROPERTIES = new Set([
|
|
150
|
+
'innerHTML',
|
|
151
|
+
'outerHTML',
|
|
152
|
+
]);
|
|
153
|
+
/**
|
|
154
|
+
* Emit a synthetic CallInfo for DOM property assignments that are XSS sinks.
|
|
155
|
+
*
|
|
156
|
+
* Matches `<obj>.<prop> = <expr>` where `<prop>` is in
|
|
157
|
+
* `DOM_XSS_ASSIGNMENT_PROPERTIES`. The synthetic call has method=`<prop>`,
|
|
158
|
+
* receiver=`<obj>` text, single argument=`<expr>`.
|
|
159
|
+
*/
|
|
160
|
+
function extractDomPropertyAssignmentSink(node) {
|
|
161
|
+
const leftNode = node.childForFieldName('left');
|
|
162
|
+
const rightNode = node.childForFieldName('right');
|
|
163
|
+
if (!leftNode || !rightNode)
|
|
164
|
+
return null;
|
|
165
|
+
if (leftNode.type !== 'member_expression')
|
|
166
|
+
return null;
|
|
167
|
+
const propertyNode = leftNode.childForFieldName('property');
|
|
168
|
+
const objectNode = leftNode.childForFieldName('object');
|
|
169
|
+
if (!propertyNode)
|
|
170
|
+
return null;
|
|
171
|
+
const propertyName = getNodeText(propertyNode);
|
|
172
|
+
if (!DOM_XSS_ASSIGNMENT_PROPERTIES.has(propertyName))
|
|
173
|
+
return null;
|
|
174
|
+
const receiver = objectNode ? getNodeText(objectNode) : null;
|
|
175
|
+
const expression = getNodeText(rightNode);
|
|
176
|
+
const { variable, literal } = analyzeJSArgument(rightNode);
|
|
177
|
+
const enclosingFunc = findJSEnclosingFunction(node);
|
|
178
|
+
return {
|
|
179
|
+
method_name: propertyName,
|
|
180
|
+
receiver,
|
|
181
|
+
arguments: [
|
|
182
|
+
{
|
|
183
|
+
position: 0,
|
|
184
|
+
expression,
|
|
185
|
+
variable,
|
|
186
|
+
literal,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
location: {
|
|
190
|
+
line: node.startPosition.row + 1,
|
|
191
|
+
column: node.startPosition.column,
|
|
192
|
+
},
|
|
193
|
+
in_method: enclosingFunc,
|
|
194
|
+
resolved: true,
|
|
195
|
+
resolution: {
|
|
196
|
+
status: 'resolved',
|
|
197
|
+
target: `DOM.${propertyName}`,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Emit a synthetic CallInfo for JSX attributes that are known XSS sinks.
|
|
203
|
+
*
|
|
204
|
+
* Currently handles `dangerouslySetInnerHTML={{ __html: expr }}` on any JSX
|
|
205
|
+
* element. The synthetic call has method `dangerouslySetInnerHTML`, no
|
|
206
|
+
* receiver, and a single argument carrying the `__html` value expression.
|
|
207
|
+
* The taint matcher then catches the call via the standard
|
|
208
|
+
* `dangerouslySetInnerHTML` sink entry in `configs/sinks/nodejs.json`.
|
|
209
|
+
*/
|
|
210
|
+
function extractJSXAttributeSink(attr) {
|
|
211
|
+
// `jsx_attribute` has children: property_identifier '=' jsx_expression
|
|
212
|
+
// Get the attribute name (first property_identifier child).
|
|
213
|
+
let nameNode = null;
|
|
214
|
+
for (let i = 0; i < attr.childCount; i++) {
|
|
215
|
+
const child = attr.child(i);
|
|
216
|
+
if (child && child.type === 'property_identifier') {
|
|
217
|
+
nameNode = child;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!nameNode)
|
|
222
|
+
return null;
|
|
223
|
+
const attrName = getNodeText(nameNode);
|
|
224
|
+
if (attrName !== 'dangerouslySetInnerHTML')
|
|
225
|
+
return null;
|
|
226
|
+
// Find the `jsx_expression` value child (the `{{__html: x}}` part).
|
|
227
|
+
let valueExpr = null;
|
|
228
|
+
for (let i = 0; i < attr.childCount; i++) {
|
|
229
|
+
const child = attr.child(i);
|
|
230
|
+
if (child && child.type === 'jsx_expression') {
|
|
231
|
+
valueExpr = child;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!valueExpr)
|
|
236
|
+
return null;
|
|
237
|
+
// Inside the jsx_expression, find the inner `object` literal, then the
|
|
238
|
+
// `pair` whose key is `__html`, then that pair's `value`.
|
|
239
|
+
let htmlValue = null;
|
|
240
|
+
for (let i = 0; i < valueExpr.childCount; i++) {
|
|
241
|
+
const inner = valueExpr.child(i);
|
|
242
|
+
if (!inner || inner.type !== 'object')
|
|
243
|
+
continue;
|
|
244
|
+
for (let j = 0; j < inner.childCount; j++) {
|
|
245
|
+
const pair = inner.child(j);
|
|
246
|
+
if (!pair || pair.type !== 'pair')
|
|
247
|
+
continue;
|
|
248
|
+
const keyNode = pair.childForFieldName('key');
|
|
249
|
+
if (!keyNode)
|
|
250
|
+
continue;
|
|
251
|
+
const keyText = getNodeText(keyNode).replace(/^["']|["']$/g, '');
|
|
252
|
+
if (keyText === '__html') {
|
|
253
|
+
htmlValue = pair.childForFieldName('value');
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (htmlValue)
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
// If the `__html` field wasn't found (or the prop was passed a spread/var
|
|
261
|
+
// like `{...props}`), fall back to using the entire jsx_expression body
|
|
262
|
+
// so the matcher still sees the data dependency.
|
|
263
|
+
if (!htmlValue) {
|
|
264
|
+
for (let i = 0; i < valueExpr.childCount; i++) {
|
|
265
|
+
const inner = valueExpr.child(i);
|
|
266
|
+
if (inner && inner.type !== '{' && inner.type !== '}') {
|
|
267
|
+
htmlValue = inner;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (!htmlValue)
|
|
273
|
+
return null;
|
|
274
|
+
const expression = getNodeText(htmlValue);
|
|
275
|
+
const { variable, literal } = analyzeJSArgument(htmlValue);
|
|
276
|
+
const enclosingFunc = findJSEnclosingFunction(attr);
|
|
277
|
+
return {
|
|
278
|
+
method_name: 'dangerouslySetInnerHTML',
|
|
279
|
+
receiver: null,
|
|
280
|
+
arguments: [
|
|
281
|
+
{
|
|
282
|
+
position: 0,
|
|
283
|
+
expression,
|
|
284
|
+
variable,
|
|
285
|
+
literal,
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
location: {
|
|
289
|
+
line: attr.startPosition.row + 1,
|
|
290
|
+
column: attr.startPosition.column,
|
|
291
|
+
},
|
|
292
|
+
in_method: enclosingFunc,
|
|
293
|
+
resolved: true,
|
|
294
|
+
resolution: {
|
|
295
|
+
status: 'resolved',
|
|
296
|
+
target: 'react.dangerouslySetInnerHTML',
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
120
300
|
function buildJSResolutionContext(tree, cache) {
|
|
121
301
|
const context = {
|
|
122
302
|
functionNames: new Set(),
|