circle-ir 3.59.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 +112 -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 +402 -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
|
@@ -4341,7 +4341,7 @@ function detectLanguage(tree) {
|
|
|
4341
4341
|
}
|
|
4342
4342
|
function extractTypes(tree, cache, language) {
|
|
4343
4343
|
const effectiveLanguage = language ?? detectLanguage(tree);
|
|
4344
|
-
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
|
|
4344
|
+
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
|
|
4345
4345
|
const isPython = effectiveLanguage === "python";
|
|
4346
4346
|
const isRust = effectiveLanguage === "rust";
|
|
4347
4347
|
if (effectiveLanguage === "go") {
|
|
@@ -5727,7 +5727,7 @@ function detectLanguageFromTree(tree, cache) {
|
|
|
5727
5727
|
function extractCalls(tree, cache, language) {
|
|
5728
5728
|
const calls = [];
|
|
5729
5729
|
const detectedLanguage = language ?? detectLanguageFromTree(tree, cache);
|
|
5730
|
-
const isJavaScript = detectedLanguage === "javascript" || detectedLanguage === "typescript";
|
|
5730
|
+
const isJavaScript = detectedLanguage === "javascript" || detectedLanguage === "typescript" || detectedLanguage === "tsx";
|
|
5731
5731
|
const isPython = detectedLanguage === "python";
|
|
5732
5732
|
const isRust = detectedLanguage === "rust";
|
|
5733
5733
|
if (detectedLanguage === "go") {
|
|
@@ -5776,8 +5776,137 @@ function extractJavaScriptCalls(tree, cache) {
|
|
|
5776
5776
|
calls.push(callInfo);
|
|
5777
5777
|
}
|
|
5778
5778
|
}
|
|
5779
|
+
const jsxAttributes = getNodesFromCache(tree.rootNode, "jsx_attribute", cache);
|
|
5780
|
+
for (const attr of jsxAttributes) {
|
|
5781
|
+
const callInfo = extractJSXAttributeSink(attr);
|
|
5782
|
+
if (callInfo) {
|
|
5783
|
+
calls.push(callInfo);
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
const assignments = getNodesFromCache(tree.rootNode, "assignment_expression", cache);
|
|
5787
|
+
for (const assign of assignments) {
|
|
5788
|
+
const callInfo = extractDomPropertyAssignmentSink(assign);
|
|
5789
|
+
if (callInfo) {
|
|
5790
|
+
calls.push(callInfo);
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5779
5793
|
return calls;
|
|
5780
5794
|
}
|
|
5795
|
+
var DOM_XSS_ASSIGNMENT_PROPERTIES = /* @__PURE__ */ new Set([
|
|
5796
|
+
"innerHTML",
|
|
5797
|
+
"outerHTML"
|
|
5798
|
+
]);
|
|
5799
|
+
function extractDomPropertyAssignmentSink(node) {
|
|
5800
|
+
const leftNode = node.childForFieldName("left");
|
|
5801
|
+
const rightNode = node.childForFieldName("right");
|
|
5802
|
+
if (!leftNode || !rightNode) return null;
|
|
5803
|
+
if (leftNode.type !== "member_expression") return null;
|
|
5804
|
+
const propertyNode = leftNode.childForFieldName("property");
|
|
5805
|
+
const objectNode = leftNode.childForFieldName("object");
|
|
5806
|
+
if (!propertyNode) return null;
|
|
5807
|
+
const propertyName = getNodeText(propertyNode);
|
|
5808
|
+
if (!DOM_XSS_ASSIGNMENT_PROPERTIES.has(propertyName)) return null;
|
|
5809
|
+
const receiver = objectNode ? getNodeText(objectNode) : null;
|
|
5810
|
+
const expression = getNodeText(rightNode);
|
|
5811
|
+
const { variable, literal } = analyzeJSArgument(rightNode);
|
|
5812
|
+
const enclosingFunc = findJSEnclosingFunction(node);
|
|
5813
|
+
return {
|
|
5814
|
+
method_name: propertyName,
|
|
5815
|
+
receiver,
|
|
5816
|
+
arguments: [
|
|
5817
|
+
{
|
|
5818
|
+
position: 0,
|
|
5819
|
+
expression,
|
|
5820
|
+
variable,
|
|
5821
|
+
literal
|
|
5822
|
+
}
|
|
5823
|
+
],
|
|
5824
|
+
location: {
|
|
5825
|
+
line: node.startPosition.row + 1,
|
|
5826
|
+
column: node.startPosition.column
|
|
5827
|
+
},
|
|
5828
|
+
in_method: enclosingFunc,
|
|
5829
|
+
resolved: true,
|
|
5830
|
+
resolution: {
|
|
5831
|
+
status: "resolved",
|
|
5832
|
+
target: `DOM.${propertyName}`
|
|
5833
|
+
}
|
|
5834
|
+
};
|
|
5835
|
+
}
|
|
5836
|
+
function extractJSXAttributeSink(attr) {
|
|
5837
|
+
let nameNode = null;
|
|
5838
|
+
for (let i2 = 0; i2 < attr.childCount; i2++) {
|
|
5839
|
+
const child = attr.child(i2);
|
|
5840
|
+
if (child && child.type === "property_identifier") {
|
|
5841
|
+
nameNode = child;
|
|
5842
|
+
break;
|
|
5843
|
+
}
|
|
5844
|
+
}
|
|
5845
|
+
if (!nameNode) return null;
|
|
5846
|
+
const attrName = getNodeText(nameNode);
|
|
5847
|
+
if (attrName !== "dangerouslySetInnerHTML") return null;
|
|
5848
|
+
let valueExpr = null;
|
|
5849
|
+
for (let i2 = 0; i2 < attr.childCount; i2++) {
|
|
5850
|
+
const child = attr.child(i2);
|
|
5851
|
+
if (child && child.type === "jsx_expression") {
|
|
5852
|
+
valueExpr = child;
|
|
5853
|
+
break;
|
|
5854
|
+
}
|
|
5855
|
+
}
|
|
5856
|
+
if (!valueExpr) return null;
|
|
5857
|
+
let htmlValue = null;
|
|
5858
|
+
for (let i2 = 0; i2 < valueExpr.childCount; i2++) {
|
|
5859
|
+
const inner = valueExpr.child(i2);
|
|
5860
|
+
if (!inner || inner.type !== "object") continue;
|
|
5861
|
+
for (let j = 0; j < inner.childCount; j++) {
|
|
5862
|
+
const pair = inner.child(j);
|
|
5863
|
+
if (!pair || pair.type !== "pair") continue;
|
|
5864
|
+
const keyNode = pair.childForFieldName("key");
|
|
5865
|
+
if (!keyNode) continue;
|
|
5866
|
+
const keyText = getNodeText(keyNode).replace(/^["']|["']$/g, "");
|
|
5867
|
+
if (keyText === "__html") {
|
|
5868
|
+
htmlValue = pair.childForFieldName("value");
|
|
5869
|
+
break;
|
|
5870
|
+
}
|
|
5871
|
+
}
|
|
5872
|
+
if (htmlValue) break;
|
|
5873
|
+
}
|
|
5874
|
+
if (!htmlValue) {
|
|
5875
|
+
for (let i2 = 0; i2 < valueExpr.childCount; i2++) {
|
|
5876
|
+
const inner = valueExpr.child(i2);
|
|
5877
|
+
if (inner && inner.type !== "{" && inner.type !== "}") {
|
|
5878
|
+
htmlValue = inner;
|
|
5879
|
+
break;
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5882
|
+
}
|
|
5883
|
+
if (!htmlValue) return null;
|
|
5884
|
+
const expression = getNodeText(htmlValue);
|
|
5885
|
+
const { variable, literal } = analyzeJSArgument(htmlValue);
|
|
5886
|
+
const enclosingFunc = findJSEnclosingFunction(attr);
|
|
5887
|
+
return {
|
|
5888
|
+
method_name: "dangerouslySetInnerHTML",
|
|
5889
|
+
receiver: null,
|
|
5890
|
+
arguments: [
|
|
5891
|
+
{
|
|
5892
|
+
position: 0,
|
|
5893
|
+
expression,
|
|
5894
|
+
variable,
|
|
5895
|
+
literal
|
|
5896
|
+
}
|
|
5897
|
+
],
|
|
5898
|
+
location: {
|
|
5899
|
+
line: attr.startPosition.row + 1,
|
|
5900
|
+
column: attr.startPosition.column
|
|
5901
|
+
},
|
|
5902
|
+
in_method: enclosingFunc,
|
|
5903
|
+
resolved: true,
|
|
5904
|
+
resolution: {
|
|
5905
|
+
status: "resolved",
|
|
5906
|
+
target: "react.dangerouslySetInnerHTML"
|
|
5907
|
+
}
|
|
5908
|
+
};
|
|
5909
|
+
}
|
|
5781
5910
|
function buildJSResolutionContext(tree, cache) {
|
|
5782
5911
|
const context = {
|
|
5783
5912
|
functionNames: /* @__PURE__ */ new Set(),
|
|
@@ -7285,7 +7414,7 @@ function detectLanguage2(tree) {
|
|
|
7285
7414
|
}
|
|
7286
7415
|
function extractImports(tree, language) {
|
|
7287
7416
|
const effectiveLanguage = language ?? detectLanguage2(tree);
|
|
7288
|
-
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
|
|
7417
|
+
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
|
|
7289
7418
|
const isPython = effectiveLanguage === "python";
|
|
7290
7419
|
const isRust = effectiveLanguage === "rust";
|
|
7291
7420
|
if (effectiveLanguage === "go") {
|
|
@@ -7977,7 +8106,7 @@ function detectLanguage3(tree) {
|
|
|
7977
8106
|
}
|
|
7978
8107
|
function buildCFG(tree, language) {
|
|
7979
8108
|
const effectiveLanguage = language ?? detectLanguage3(tree);
|
|
7980
|
-
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
|
|
8109
|
+
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
|
|
7981
8110
|
const allBlocks = [];
|
|
7982
8111
|
const allEdges = [];
|
|
7983
8112
|
let blockIdCounter = 0;
|
|
@@ -8552,7 +8681,7 @@ function detectLanguage4(tree) {
|
|
|
8552
8681
|
}
|
|
8553
8682
|
function buildDFG(tree, cache, language) {
|
|
8554
8683
|
const effectiveLanguage = language ?? detectLanguage4(tree);
|
|
8555
|
-
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
|
|
8684
|
+
const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
|
|
8556
8685
|
if (isJavaScript) {
|
|
8557
8686
|
return buildJavaScriptDFG(tree, cache);
|
|
8558
8687
|
}
|
|
@@ -9360,7 +9489,18 @@ function buildBashDFG(tree) {
|
|
|
9360
9489
|
if (varNameNode) {
|
|
9361
9490
|
const varName = getNodeText(varNameNode);
|
|
9362
9491
|
if (varName && !varName.startsWith("?") && !varName.startsWith("#")) {
|
|
9363
|
-
|
|
9492
|
+
let reachingDef = findReachingDef(varName, scopeStack);
|
|
9493
|
+
if (reachingDef === null && !positionalParams.includes(varName)) {
|
|
9494
|
+
const def = {
|
|
9495
|
+
id: defIdCounter++,
|
|
9496
|
+
variable: varName,
|
|
9497
|
+
line: 0,
|
|
9498
|
+
kind: "param"
|
|
9499
|
+
};
|
|
9500
|
+
defs.push(def);
|
|
9501
|
+
scopeStack[0].set(varName, def.id);
|
|
9502
|
+
reachingDef = def.id;
|
|
9503
|
+
}
|
|
9364
9504
|
uses.push({
|
|
9365
9505
|
id: useIdCounter++,
|
|
9366
9506
|
variable: varName,
|
|
@@ -9373,7 +9513,18 @@ function buildBashDFG(tree) {
|
|
|
9373
9513
|
const varNameNode = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
9374
9514
|
if (varNameNode && varNameNode.type === "variable_name") {
|
|
9375
9515
|
const varName = getNodeText(varNameNode);
|
|
9376
|
-
|
|
9516
|
+
let reachingDef = findReachingDef(varName, scopeStack);
|
|
9517
|
+
if (reachingDef === null && !positionalParams.includes(varName)) {
|
|
9518
|
+
const def = {
|
|
9519
|
+
id: defIdCounter++,
|
|
9520
|
+
variable: varName,
|
|
9521
|
+
line: 0,
|
|
9522
|
+
kind: "param"
|
|
9523
|
+
};
|
|
9524
|
+
defs.push(def);
|
|
9525
|
+
scopeStack[0].set(varName, def.id);
|
|
9526
|
+
reachingDef = def.id;
|
|
9527
|
+
}
|
|
9377
9528
|
uses.push({
|
|
9378
9529
|
id: useIdCounter++,
|
|
9379
9530
|
variable: varName,
|
|
@@ -9835,7 +9986,7 @@ var FRAMEWORK_MODULE_PATTERNS = [
|
|
|
9835
9986
|
/^@nestjs\/core$/
|
|
9836
9987
|
];
|
|
9837
9988
|
function extractRuntimeRegistrations(tree, cache, language, imports) {
|
|
9838
|
-
if (language === "javascript" || language === "typescript") {
|
|
9989
|
+
if (language === "javascript" || language === "typescript" || language === "tsx") {
|
|
9839
9990
|
return extractJSRuntimeRegistrations(tree, cache, imports);
|
|
9840
9991
|
}
|
|
9841
9992
|
if (language === "python") {
|
|
@@ -10786,8 +10937,12 @@ var DEFAULT_SOURCES = [
|
|
|
10786
10937
|
{ method: "get", class: "cookies", type: "http_cookie", severity: "high", return_tainted: true },
|
|
10787
10938
|
{ property: "json", object: "request", type: "http_body", severity: "high", property_tainted: true },
|
|
10788
10939
|
{ property: "data", object: "request", type: "http_body", severity: "high", property_tainted: true },
|
|
10940
|
+
{ property: "stream", object: "request", type: "http_body", severity: "high", property_tainted: true },
|
|
10789
10941
|
{ property: "path", object: "request", type: "http_path", severity: "medium", property_tainted: true },
|
|
10790
10942
|
{ property: "query_string", object: "request", type: "http_query", severity: "high", property_tainted: true },
|
|
10943
|
+
// Flask request.get_data() — raw request bytes (method form, parallel to request.data property)
|
|
10944
|
+
{ method: "get_data", class: "request", type: "http_body", severity: "high", return_tainted: true },
|
|
10945
|
+
{ method: "get_json", class: "request", type: "http_body", severity: "high", return_tainted: true },
|
|
10791
10946
|
// Django request object
|
|
10792
10947
|
{ method: "get", class: "GET", type: "http_param", severity: "high", return_tainted: true },
|
|
10793
10948
|
{ method: "get", class: "POST", type: "http_param", severity: "high", return_tainted: true },
|
|
@@ -10999,14 +11154,19 @@ var DEFAULT_SINKS = [
|
|
|
10999
11154
|
{ method: "setExecutable", class: "ExecTask", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11000
11155
|
{ method: "setCommand", class: "ExecTask", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11001
11156
|
{ method: "execute", class: "Java", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11002
|
-
// Shell/Bash utilities
|
|
11003
|
-
|
|
11004
|
-
|
|
11005
|
-
|
|
11006
|
-
|
|
11007
|
-
{
|
|
11008
|
-
{ method: "
|
|
11009
|
-
{ method: "
|
|
11157
|
+
// Shell/Bash utilities — these are method-call sinks in host languages
|
|
11158
|
+
// (Java Runtime/ProcessBuilder, JS child_process spawn/exec, Python subprocess, etc.).
|
|
11159
|
+
// When the analyzed file IS a bash/shell script, the bash plugin's per-flag entries
|
|
11160
|
+
// (argPositions: [1] for `bash -c <cmd>`) MUST win. Restrict these generic entries
|
|
11161
|
+
// to non-shell languages so they don't collide on the dedup key
|
|
11162
|
+
// `${location}:${call.location.line}:${pattern.cwe}`.
|
|
11163
|
+
{ method: "bash", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11164
|
+
{ method: "shell", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11165
|
+
{ method: "sh", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11166
|
+
{ method: "spawn", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11167
|
+
{ method: "fork", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11168
|
+
{ method: "popen", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11169
|
+
{ method: "system", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11010
11170
|
// Apache Commons Exec
|
|
11011
11171
|
// Note: bare class 'Executor' removed (see comment above) — DefaultExecutor matched explicitly.
|
|
11012
11172
|
{ method: "setCommandline", class: "DefaultExecutor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
@@ -11096,6 +11256,12 @@ var DEFAULT_SINKS = [
|
|
|
11096
11256
|
{ method: "unzip", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
|
|
11097
11257
|
{ method: "extract", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
|
|
11098
11258
|
{ method: "extractAll", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
|
|
11259
|
+
// Python zipfile/tarfile use lowercase extractall (PEP 8 naming)
|
|
11260
|
+
{ method: "extractall", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0], languages: ["python"] },
|
|
11261
|
+
// Python zipfile.ZipFile(path) — tainted archive path enables Zip-Slip via malicious archive
|
|
11262
|
+
{ method: "ZipFile", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
|
|
11263
|
+
// Flask send_from_directory: untrusted filename can escape directory via ../
|
|
11264
|
+
{ method: "send_from_directory", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [1], languages: ["python"] },
|
|
11099
11265
|
{ method: "unjar", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
|
|
11100
11266
|
// Additional file constructors — BufferedReader(Reader) is NOT a path traversal sink; it wraps a Reader, not a file path
|
|
11101
11267
|
{ method: "PrintWriter", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
@@ -12180,16 +12346,42 @@ var DEFAULT_SINKS = [
|
|
|
12180
12346
|
// value position so a tainted variable is detected.
|
|
12181
12347
|
{ method: "Set", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
|
|
12182
12348
|
{ method: "Add", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
|
|
12183
|
-
// Mass-assignment (CWE-915) — Sprint 6, #86.
|
|
12184
|
-
// JS Object.assign(target, ...sources)
|
|
12185
|
-
//
|
|
12186
|
-
//
|
|
12187
|
-
|
|
12188
|
-
//
|
|
12189
|
-
|
|
12190
|
-
|
|
12349
|
+
// Mass-assignment (CWE-915 / CWE-1321) — Sprint 6, #86; cognium-dev #68 Sprint 10.
|
|
12350
|
+
// JS Object.assign(target, ...sources), `_.merge`, `_.extend`, `$.extend`,
|
|
12351
|
+
// `Object.defineProperty` — when fed an attacker-controlled bag, they write
|
|
12352
|
+
// arbitrary keys onto the target (or, for `__proto__`/`constructor.prototype`,
|
|
12353
|
+
// pollute the prototype chain). The CWE is CWE-1321 (Prototype Pollution),
|
|
12354
|
+
// which subsumes mass assignment for JS sinks operating on plain Objects.
|
|
12355
|
+
// We keep the existing `mass_assignment` SinkType so consumers route the
|
|
12356
|
+
// findings the same way; only the CWE shifts to flag prototype-pollution.
|
|
12357
|
+
{ method: "assign", class: "Object", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
12358
|
+
{ method: "defineProperty", class: "Object", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2], languages: ["javascript", "typescript"] },
|
|
12359
|
+
{ method: "defineProperties", class: "Object", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1], languages: ["javascript", "typescript"] },
|
|
12360
|
+
// Lodash bulk-merge helpers behave identically. `_.merge` and `lodash.merge`
|
|
12361
|
+
// are aliases — match both receivers.
|
|
12362
|
+
{ method: "merge", class: "_", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
12363
|
+
{ method: "merge", class: "lodash", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
12364
|
+
{ method: "extend", class: "_", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
12365
|
+
{ method: "extend", class: "lodash", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
12366
|
+
{ method: "defaultsDeep", class: "_", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
12367
|
+
{ method: "defaultsDeep", class: "lodash", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
12191
12368
|
// jQuery $.extend(target, source) (legacy).
|
|
12192
|
-
{ method: "extend", class: "$", type: "mass_assignment", cwe: "CWE-
|
|
12369
|
+
{ method: "extend", class: "$", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
12370
|
+
{ method: "extend", class: "jQuery", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
|
|
12371
|
+
// DOM-XSS via property assignment (CWE-79) — cognium-dev #68 Sprint 10.
|
|
12372
|
+
// `el.innerHTML = tainted` / `el.outerHTML = tainted`. The JS call extractor
|
|
12373
|
+
// emits a synthetic CallInfo with method=`innerHTML`/`outerHTML` for each
|
|
12374
|
+
// matching assignment_expression. These classless entries catch them.
|
|
12375
|
+
{ method: "innerHTML", type: "xss", cwe: "CWE-79", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
12376
|
+
{ method: "outerHTML", type: "xss", cwe: "CWE-79", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
12377
|
+
// node-serialize.unserialize (CWE-502) — cognium-dev #68 Sprint 10.
|
|
12378
|
+
// The node-serialize package evaluates `_$$ND_FUNC$$_` IIFE payloads on
|
|
12379
|
+
// decode, turning untrusted input into RCE. Match both receiver-bound
|
|
12380
|
+
// calls (`serialize.unserialize(x)`) and destructured imports
|
|
12381
|
+
// (`const { unserialize } = require('node-serialize')`).
|
|
12382
|
+
{ method: "unserialize", class: "serialize", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
12383
|
+
{ method: "unserialize", class: "node-serialize", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
12384
|
+
{ method: "unserialize", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] }
|
|
12193
12385
|
];
|
|
12194
12386
|
var DEFAULT_SANITIZERS = [
|
|
12195
12387
|
// SQL Injection - proper parameter binding sanitizes input
|
|
@@ -12883,7 +13075,12 @@ function matchesSourcePattern(call, pattern) {
|
|
|
12883
13075
|
if (call.receiver_type && call.receiver_type === pattern.class) {
|
|
12884
13076
|
} else if (call.receiver_type_fqn && call.receiver_type_fqn.endsWith("." + pattern.class)) {
|
|
12885
13077
|
} else if (!call.receiver) {
|
|
12886
|
-
|
|
13078
|
+
const target = call.resolution?.target;
|
|
13079
|
+
const expectedTail = `${pattern.class}.${pattern.method}`;
|
|
13080
|
+
if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
|
|
13081
|
+
} else {
|
|
13082
|
+
return false;
|
|
13083
|
+
}
|
|
12887
13084
|
} else if (!receiverMightBeClass(call.receiver, pattern.class)) {
|
|
12888
13085
|
return false;
|
|
12889
13086
|
}
|
|
@@ -13150,7 +13347,12 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
|
|
|
13150
13347
|
}
|
|
13151
13348
|
return false;
|
|
13152
13349
|
} else if (!call.receiver && !call.receiver_type) {
|
|
13153
|
-
|
|
13350
|
+
const target = call.resolution?.target;
|
|
13351
|
+
const expectedTail = `${pattern.class}.${pattern.method}`;
|
|
13352
|
+
if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
|
|
13353
|
+
} else {
|
|
13354
|
+
return false;
|
|
13355
|
+
}
|
|
13154
13356
|
}
|
|
13155
13357
|
}
|
|
13156
13358
|
if (!pattern.class && call.receiver) {
|
|
@@ -14939,6 +15141,21 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
|
|
|
14939
15141
|
});
|
|
14940
15142
|
}
|
|
14941
15143
|
}
|
|
15144
|
+
if (source.variable) {
|
|
15145
|
+
const paramDefs = defsByLine.get(0) ?? [];
|
|
15146
|
+
for (const def of paramDefs) {
|
|
15147
|
+
if (def.kind === "param" && def.variable === source.variable) {
|
|
15148
|
+
tainted.push({
|
|
15149
|
+
variable: def.variable,
|
|
15150
|
+
defId: def.id,
|
|
15151
|
+
line: def.line,
|
|
15152
|
+
sourceType: source.type,
|
|
15153
|
+
sourceLine: source.line,
|
|
15154
|
+
confidence: source.confidence
|
|
15155
|
+
});
|
|
15156
|
+
}
|
|
15157
|
+
}
|
|
15158
|
+
}
|
|
14942
15159
|
}
|
|
14943
15160
|
return tainted;
|
|
14944
15161
|
}
|
|
@@ -15107,6 +15324,13 @@ function analyzeInterprocedural(graphOrTypes, callsOrSources, dfgOrSinks, source
|
|
|
15107
15324
|
for (const def of graph.defsAtLine(source.line)) {
|
|
15108
15325
|
seedIds.add(def.id);
|
|
15109
15326
|
}
|
|
15327
|
+
if (source.variable) {
|
|
15328
|
+
for (const def of graph.defsAtLine(0)) {
|
|
15329
|
+
if (def.kind === "param" && def.variable === source.variable) {
|
|
15330
|
+
seedIds.add(def.id);
|
|
15331
|
+
}
|
|
15332
|
+
}
|
|
15333
|
+
}
|
|
15110
15334
|
}
|
|
15111
15335
|
const taintedDefIds = graph.propagateTaintedDefIds(seedIds);
|
|
15112
15336
|
const taintedVarsFromCP = options.taintedVariables ?? /* @__PURE__ */ new Set();
|
|
@@ -15258,7 +15482,36 @@ function analyzeInterprocedural(graphOrTypes, callsOrSources, dfgOrSinks, source
|
|
|
15258
15482
|
const targetMethod = getMethodNode(methodNodes, call.method_name);
|
|
15259
15483
|
if (!targetMethod) {
|
|
15260
15484
|
if (taintedArgPositions.length > 0 && !collectionMethods.has(call.method_name) && !sanitizerMethods.has(call.method_name) && !safeUtilityMethods.has(call.method_name)) {
|
|
15261
|
-
const
|
|
15485
|
+
const isBash = graph.ir.meta.language === "bash";
|
|
15486
|
+
const bashSafeBuiltins = /* @__PURE__ */ new Set([
|
|
15487
|
+
"echo",
|
|
15488
|
+
"printf",
|
|
15489
|
+
"test",
|
|
15490
|
+
"[",
|
|
15491
|
+
"[[",
|
|
15492
|
+
"true",
|
|
15493
|
+
"false",
|
|
15494
|
+
":",
|
|
15495
|
+
"declare",
|
|
15496
|
+
"local",
|
|
15497
|
+
"export",
|
|
15498
|
+
"readonly",
|
|
15499
|
+
"typeset"
|
|
15500
|
+
]);
|
|
15501
|
+
if (isBash && bashSafeBuiltins.has(call.method_name)) {
|
|
15502
|
+
continue;
|
|
15503
|
+
}
|
|
15504
|
+
const sink = isBash ? {
|
|
15505
|
+
type: "command_injection",
|
|
15506
|
+
cwe: "CWE-78",
|
|
15507
|
+
location: `Tainted data (${taintedArgVars.join(", ")}) passed unquoted to shell utility ${call.method_name}`,
|
|
15508
|
+
line: call.location.line,
|
|
15509
|
+
confidence: 0.6,
|
|
15510
|
+
method: call.method_name,
|
|
15511
|
+
argPositions: taintedArgPositions
|
|
15512
|
+
} : {
|
|
15513
|
+
// Create an "external_taint_escape" sink for this call
|
|
15514
|
+
// This represents tainted data being passed to code we can't analyze
|
|
15262
15515
|
type: "external_taint_escape",
|
|
15263
15516
|
cwe: "CWE-668",
|
|
15264
15517
|
// Exposure of Resource to Wrong Sphere
|
|
@@ -18154,6 +18407,9 @@ var DefaultLanguageRegistry = class {
|
|
|
18154
18407
|
}
|
|
18155
18408
|
}
|
|
18156
18409
|
get(language) {
|
|
18410
|
+
if (language === "tsx") {
|
|
18411
|
+
return this.plugins.get("javascript");
|
|
18412
|
+
}
|
|
18157
18413
|
return this.plugins.get(language);
|
|
18158
18414
|
}
|
|
18159
18415
|
getForFile(filePath) {
|
|
@@ -20590,6 +20846,23 @@ var BashPlugin = class extends BaseLanguagePlugin {
|
|
|
20590
20846
|
cwe: "CWE-918",
|
|
20591
20847
|
severity: "high",
|
|
20592
20848
|
argPositions: [0]
|
|
20849
|
+
},
|
|
20850
|
+
// File inclusion — `source` and POSIX `.` execute arbitrary shell code
|
|
20851
|
+
// from the file at the supplied path. With user-controlled input this is
|
|
20852
|
+
// an RCE primitive equivalent to eval() on file contents (CWE-98).
|
|
20853
|
+
{
|
|
20854
|
+
method: "source",
|
|
20855
|
+
type: "path_traversal",
|
|
20856
|
+
cwe: "CWE-98",
|
|
20857
|
+
severity: "critical",
|
|
20858
|
+
argPositions: [0]
|
|
20859
|
+
},
|
|
20860
|
+
{
|
|
20861
|
+
method: ".",
|
|
20862
|
+
type: "path_traversal",
|
|
20863
|
+
cwe: "CWE-98",
|
|
20864
|
+
severity: "critical",
|
|
20865
|
+
argPositions: [0]
|
|
20593
20866
|
}
|
|
20594
20867
|
];
|
|
20595
20868
|
}
|
|
@@ -21739,6 +22012,7 @@ function mergeHtmlResults(htmlMeta, scriptResults, attributeFindings) {
|
|
|
21739
22012
|
const allSources = [];
|
|
21740
22013
|
const allSinks = [];
|
|
21741
22014
|
const allSanitizers = [];
|
|
22015
|
+
const allFlows = [];
|
|
21742
22016
|
const allImports = [];
|
|
21743
22017
|
const allExports = [];
|
|
21744
22018
|
const allFindings = [];
|
|
@@ -21824,6 +22098,14 @@ function mergeHtmlResults(htmlMeta, scriptResults, attributeFindings) {
|
|
|
21824
22098
|
line: sanitizer.line + lineShift
|
|
21825
22099
|
});
|
|
21826
22100
|
}
|
|
22101
|
+
for (const flow of ir.taint.flows ?? []) {
|
|
22102
|
+
allFlows.push({
|
|
22103
|
+
...flow,
|
|
22104
|
+
source_line: flow.source_line + lineShift,
|
|
22105
|
+
sink_line: flow.sink_line + lineShift,
|
|
22106
|
+
path: flow.path.map((step) => ({ ...step, line: step.line + lineShift }))
|
|
22107
|
+
});
|
|
22108
|
+
}
|
|
21827
22109
|
for (const imp of ir.imports) {
|
|
21828
22110
|
allImports.push({
|
|
21829
22111
|
...imp,
|
|
@@ -21843,7 +22125,8 @@ function mergeHtmlResults(htmlMeta, scriptResults, attributeFindings) {
|
|
|
21843
22125
|
const taint = {
|
|
21844
22126
|
sources: allSources,
|
|
21845
22127
|
sinks: allSinks,
|
|
21846
|
-
sanitizers: allSanitizers.length > 0 ? allSanitizers : void 0
|
|
22128
|
+
sanitizers: allSanitizers.length > 0 ? allSanitizers : void 0,
|
|
22129
|
+
flows: allFlows.length > 0 ? allFlows : void 0
|
|
21847
22130
|
};
|
|
21848
22131
|
const cfg = {
|
|
21849
22132
|
blocks: allCfgBlocks,
|
|
@@ -22044,6 +22327,7 @@ var LanguageSourcesPass = class {
|
|
|
22044
22327
|
const constProp = ctx.getResult("constant-propagation");
|
|
22045
22328
|
const additionalSources = [];
|
|
22046
22329
|
const additionalSinks = [];
|
|
22330
|
+
const additionalSanitizers = [];
|
|
22047
22331
|
additionalSources.push(...findGetterSources(types, constProp.instanceFieldTaint, code));
|
|
22048
22332
|
additionalSources.push(...findOopFieldReadSources(types, code, language));
|
|
22049
22333
|
additionalSources.push(...findJavaScriptAssignmentSources(code, language));
|
|
@@ -22097,9 +22381,10 @@ var LanguageSourcesPass = class {
|
|
|
22097
22381
|
for (const finding of bashFindings) {
|
|
22098
22382
|
ctx.addFinding(finding);
|
|
22099
22383
|
}
|
|
22384
|
+
additionalSanitizers.push(...findBashRegexAllowlistSanitizers(code));
|
|
22100
22385
|
}
|
|
22101
22386
|
attachSourceLineCode(additionalSources, additionalSinks, code);
|
|
22102
|
-
return { additionalSources, additionalSinks, pyTaintedVars, pySanitizedVars, jsTaintedVars };
|
|
22387
|
+
return { additionalSources, additionalSinks, additionalSanitizers, pyTaintedVars, pySanitizedVars, jsTaintedVars };
|
|
22103
22388
|
}
|
|
22104
22389
|
};
|
|
22105
22390
|
function findGetterSources(types, instanceFieldTaint, _sourceCode) {
|
|
@@ -22327,55 +22612,55 @@ function buildPythonTaintedVars(sourceCode) {
|
|
|
22327
22612
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
22328
22613
|
const line = lines[i2];
|
|
22329
22614
|
if (line.trimStart().startsWith("#")) continue;
|
|
22330
|
-
const subscriptAssign = line.match(/^\s*(\
|
|
22615
|
+
const subscriptAssign = line.match(/^\s*([\p{L}\p{N}_]+)\[(['"])([^'"]+)\2\]\s*=\s*(.+)$/u);
|
|
22331
22616
|
if (subscriptAssign) {
|
|
22332
22617
|
const [, container, , key, rhs2] = subscriptAssign;
|
|
22333
|
-
const isTaintedRhs = [...tainted.keys()].some((v) => new RegExp(
|
|
22618
|
+
const isTaintedRhs = [...tainted.keys()].some((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, "u").test(rhs2));
|
|
22334
22619
|
if (isTaintedRhs) containerTainted.set(`${container}['${key}']`, i2 + 1);
|
|
22335
22620
|
continue;
|
|
22336
22621
|
}
|
|
22337
|
-
const setCallMatch = line.match(/^\s*(\
|
|
22622
|
+
const setCallMatch = line.match(/^\s*([\p{L}\p{N}_]+)\.set\s*\(\s*(['"])([^'"]+)\2\s*,\s*(['"])([^'"]+)\4\s*,\s*(.+?)\s*\)$/u);
|
|
22338
22623
|
if (setCallMatch) {
|
|
22339
22624
|
const [, obj, , section, , key, rhs2] = setCallMatch;
|
|
22340
|
-
const isTaintedRhs = [...tainted.keys()].some((v) => new RegExp(
|
|
22625
|
+
const isTaintedRhs = [...tainted.keys()].some((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, "u").test(rhs2));
|
|
22341
22626
|
if (isTaintedRhs) containerTainted.set(`${obj}['${section}']['${key}']`, i2 + 1);
|
|
22342
22627
|
continue;
|
|
22343
22628
|
}
|
|
22344
|
-
const containerAppendMatch = line.match(/^\s*(\
|
|
22629
|
+
const containerAppendMatch = line.match(/^\s*([\p{L}\p{N}_]+)\.(append|extend|insert|add|push|put|appendleft)\s*\(\s*(.+?)\s*\)\s*$/u);
|
|
22345
22630
|
if (containerAppendMatch) {
|
|
22346
22631
|
const [, receiver, , argExpr] = containerAppendMatch;
|
|
22347
|
-
const argIsTainted = [...tainted.keys()].some((v) => new RegExp(
|
|
22632
|
+
const argIsTainted = [...tainted.keys()].some((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, "u").test(argExpr));
|
|
22348
22633
|
const argIsDirectSource = PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(argExpr));
|
|
22349
22634
|
if (argIsTainted || argIsDirectSource) tainted.set(receiver, tainted.get(receiver) ?? i2 + 1);
|
|
22350
22635
|
continue;
|
|
22351
22636
|
}
|
|
22352
|
-
const augAssign = line.match(/^\s*(\
|
|
22637
|
+
const augAssign = line.match(/^\s*([\p{L}\p{N}_]+)\s*\+=\s*(.+)$/u);
|
|
22353
22638
|
if (augAssign) {
|
|
22354
22639
|
const [, augLhs, augRhs] = augAssign;
|
|
22355
|
-
const rhsTainted = [...tainted.keys()].some((v) => new RegExp(
|
|
22640
|
+
const rhsTainted = [...tainted.keys()].some((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, "u").test(augRhs));
|
|
22356
22641
|
if (rhsTainted || tainted.has(augLhs)) tainted.set(augLhs, tainted.get(augLhs) ?? i2 + 1);
|
|
22357
22642
|
continue;
|
|
22358
22643
|
}
|
|
22359
|
-
const forLoopMatch = line.match(/^\s*for\s+(\
|
|
22644
|
+
const forLoopMatch = line.match(/^\s*for\s+([\p{L}\p{N}_]+)\s+in\s+(.+?)(?:\s*:\s*)?$/u);
|
|
22360
22645
|
if (forLoopMatch) {
|
|
22361
22646
|
const [, iterVar, iterExpr] = forLoopMatch;
|
|
22362
22647
|
const isDirectSource2 = PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(iterExpr));
|
|
22363
|
-
const isPropagated = [...tainted.keys()].some((v) => new RegExp(
|
|
22648
|
+
const isPropagated = [...tainted.keys()].some((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, "u").test(iterExpr));
|
|
22364
22649
|
if (isDirectSource2 || isPropagated) tainted.set(iterVar, i2 + 1);
|
|
22365
22650
|
continue;
|
|
22366
22651
|
}
|
|
22367
|
-
const assignMatch = line.match(/^\s*(\
|
|
22652
|
+
const assignMatch = line.match(/^\s*([\p{L}\p{N}_]+)\s*=\s*(.+)$/u);
|
|
22368
22653
|
if (!assignMatch) continue;
|
|
22369
22654
|
const [, lhs, rhs] = assignMatch;
|
|
22370
22655
|
const isDirectSource = PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(rhs));
|
|
22371
22656
|
let propagatedFrom;
|
|
22372
|
-
const dictAccessMatch = rhs.trim().match(/^(\
|
|
22657
|
+
const dictAccessMatch = rhs.trim().match(/^([\p{L}\p{N}_]+)\[(['"])([^'"]+)\2\]$/u);
|
|
22373
22658
|
if (dictAccessMatch) {
|
|
22374
22659
|
const [, container, , key] = dictAccessMatch;
|
|
22375
22660
|
if (containerTainted.has(`${container}['${key}']`)) propagatedFrom = `${container}['${key}']`;
|
|
22376
22661
|
}
|
|
22377
22662
|
if (!propagatedFrom) {
|
|
22378
|
-
const confGetMatch = rhs.trim().match(/^(\
|
|
22663
|
+
const confGetMatch = rhs.trim().match(/^([\p{L}\p{N}_]+)\.get\s*\(\s*(['"])([^'"]+)\2\s*,\s*(['"])([^'"]+)\4\s*\)$/u);
|
|
22379
22664
|
if (confGetMatch) {
|
|
22380
22665
|
const [, obj, , section, , key] = confGetMatch;
|
|
22381
22666
|
if (containerTainted.has(`${obj}['${section}']['${key}']`)) propagatedFrom = `${obj}['${section}']['${key}']`;
|
|
@@ -22383,7 +22668,7 @@ function buildPythonTaintedVars(sourceCode) {
|
|
|
22383
22668
|
}
|
|
22384
22669
|
if (!propagatedFrom) {
|
|
22385
22670
|
const isSafeEnvRead = /\bos\.environ\.get\s*\(/.test(rhs) || /\bos\.getenv\s*\(/.test(rhs);
|
|
22386
|
-
if (!isSafeEnvRead) propagatedFrom = [...tainted.keys()].find((v) => new RegExp(
|
|
22671
|
+
if (!isSafeEnvRead) propagatedFrom = [...tainted.keys()].find((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, "u").test(rhs));
|
|
22387
22672
|
}
|
|
22388
22673
|
if (isDirectSource) {
|
|
22389
22674
|
tainted.set(lhs, i2 + 1);
|
|
@@ -22744,6 +23029,52 @@ function findBashPatternFindings(sourceCode, file) {
|
|
|
22744
23029
|
}
|
|
22745
23030
|
return findings;
|
|
22746
23031
|
}
|
|
23032
|
+
function findBashRegexAllowlistSanitizers(code) {
|
|
23033
|
+
const sanitizers = [];
|
|
23034
|
+
const lines = code.split("\n");
|
|
23035
|
+
const guardRe = /^\s*if\s+\[\[\s*!\s*"?\$\{?(\w+)\}?"?\s*=~\s*(\S+)\s*\]\]\s*;\s*then\s+(exit|return|die)\b/;
|
|
23036
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
23037
|
+
const m = guardRe.exec(lines[i2]);
|
|
23038
|
+
if (!m) continue;
|
|
23039
|
+
const regexLiteral = m[2];
|
|
23040
|
+
if (!isSafeBashAllowlistRegex(regexLiteral)) continue;
|
|
23041
|
+
const ifLine1Indexed = i2 + 1;
|
|
23042
|
+
for (let l = ifLine1Indexed + 1; l <= lines.length; l++) {
|
|
23043
|
+
sanitizers.push({
|
|
23044
|
+
type: "regex_allowlist",
|
|
23045
|
+
method: "=~",
|
|
23046
|
+
line: l,
|
|
23047
|
+
sanitizes: [
|
|
23048
|
+
"command_injection",
|
|
23049
|
+
"path_traversal",
|
|
23050
|
+
"sql_injection",
|
|
23051
|
+
"code_injection",
|
|
23052
|
+
"ssrf",
|
|
23053
|
+
"xss",
|
|
23054
|
+
"open_redirect",
|
|
23055
|
+
"log_injection"
|
|
23056
|
+
]
|
|
23057
|
+
});
|
|
23058
|
+
}
|
|
23059
|
+
}
|
|
23060
|
+
return sanitizers;
|
|
23061
|
+
}
|
|
23062
|
+
function isSafeBashAllowlistRegex(literal) {
|
|
23063
|
+
if (!literal.startsWith("^") || !literal.endsWith("$")) return false;
|
|
23064
|
+
const body2 = literal.slice(1, -1);
|
|
23065
|
+
if (body2.length === 0) return false;
|
|
23066
|
+
if (body2.includes(".*") || body2.includes(".+")) return false;
|
|
23067
|
+
if (body2.includes("|")) return false;
|
|
23068
|
+
if (/\\\d/.test(body2)) return false;
|
|
23069
|
+
const safeToken = /\[[^\]]+\][+*?]?|\\.|[A-Za-z0-9_\-./]|[+*?]/g;
|
|
23070
|
+
let consumed = 0;
|
|
23071
|
+
let match;
|
|
23072
|
+
while ((match = safeToken.exec(body2)) !== null) {
|
|
23073
|
+
if (match.index !== consumed) return false;
|
|
23074
|
+
consumed += match[0].length;
|
|
23075
|
+
}
|
|
23076
|
+
return consumed === body2.length;
|
|
23077
|
+
}
|
|
22747
23078
|
|
|
22748
23079
|
// src/analysis/passes/sink-filter-pass.ts
|
|
22749
23080
|
var JS_XSS_SANITIZERS = [
|
|
@@ -22783,7 +23114,10 @@ var SinkFilterPass = class {
|
|
|
22783
23114
|
sinks.push(s);
|
|
22784
23115
|
}
|
|
22785
23116
|
}
|
|
22786
|
-
const sanitizers =
|
|
23117
|
+
const sanitizers = [
|
|
23118
|
+
...taintMatcher.sanitizers,
|
|
23119
|
+
...langSources.additionalSanitizers ?? []
|
|
23120
|
+
];
|
|
22787
23121
|
let filtered = sinks.filter((sink) => !constProp.unreachableLines.has(sink.line));
|
|
22788
23122
|
filtered = filterCleanArraySinks(filtered, calls, constProp.taintedArrayElements, constProp.symbols);
|
|
22789
23123
|
filtered = filterCleanVariableSinks(
|
|
@@ -23513,7 +23847,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
23513
23847
|
for (const s of sourcesWithVar) {
|
|
23514
23848
|
if (reCache.has(s.variable)) continue;
|
|
23515
23849
|
const escaped = s.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23516
|
-
reCache.set(s.variable, new RegExp(
|
|
23850
|
+
reCache.set(s.variable, new RegExp(`(?<![\\p{L}\\p{N}_])${escaped}(?![\\p{L}\\p{N}_])`, "u"));
|
|
23517
23851
|
}
|
|
23518
23852
|
const callsByLine = /* @__PURE__ */ new Map();
|
|
23519
23853
|
for (const call of calls) {
|
|
@@ -24037,6 +24371,7 @@ function isPublicMethod(method, language) {
|
|
|
24037
24371
|
return method.modifiers.includes("public");
|
|
24038
24372
|
case "javascript":
|
|
24039
24373
|
case "typescript":
|
|
24374
|
+
case "tsx":
|
|
24040
24375
|
return !method.modifiers.includes("private") && !method.modifiers.includes("protected");
|
|
24041
24376
|
case "python":
|
|
24042
24377
|
return !method.name.startsWith("_");
|
|
@@ -24055,7 +24390,7 @@ var MissingPublicDocPass = class {
|
|
|
24055
24390
|
if (UTIL_DIR_RE.test(graph.ir.meta.file)) {
|
|
24056
24391
|
return { missingDocMethods: [], missingDocTypes: [] };
|
|
24057
24392
|
}
|
|
24058
|
-
if (!["java", "javascript", "typescript", "python"].includes(language)) {
|
|
24393
|
+
if (!["java", "javascript", "typescript", "tsx", "python"].includes(language)) {
|
|
24059
24394
|
return { missingDocMethods: [], missingDocTypes: [] };
|
|
24060
24395
|
}
|
|
24061
24396
|
const lines = code.split("\n");
|
|
@@ -24558,6 +24893,7 @@ function hasDeclKeyword(lineText, language) {
|
|
|
24558
24893
|
return /\b(?:int|long|float|double|boolean|byte|char|short|var|final)\b/.test(lineText) || /\b[A-Z]\w*(?:<[^>]*>)?\s+\w/.test(lineText);
|
|
24559
24894
|
case "javascript":
|
|
24560
24895
|
case "typescript":
|
|
24896
|
+
case "tsx":
|
|
24561
24897
|
return /\b(?:let|const|var)\s+[\w{[]/.test(lineText);
|
|
24562
24898
|
case "rust":
|
|
24563
24899
|
return /\blet\s+(?:mut\s+)?\w/.test(lineText);
|
|
@@ -28496,6 +28832,7 @@ var WeakRandomPass = class {
|
|
|
28496
28832
|
return "Use the `secrets` module (`secrets.token_bytes`, `secrets.token_hex`, `secrets.choice`, `secrets.randbelow`).";
|
|
28497
28833
|
case "javascript":
|
|
28498
28834
|
case "typescript":
|
|
28835
|
+
case "tsx":
|
|
28499
28836
|
return "Use `crypto.randomBytes(n)` (Node.js) or `crypto.getRandomValues(typedArray)` (browser).";
|
|
28500
28837
|
case "go":
|
|
28501
28838
|
return "Use `crypto/rand` instead of `math/rand`. Example: `b := make([]byte, 32); _, _ = rand.Read(b)` (where `rand` is `crypto/rand`).";
|
|
@@ -28532,7 +28869,7 @@ var WeakRandomPass = class {
|
|
|
28532
28869
|
}
|
|
28533
28870
|
return null;
|
|
28534
28871
|
}
|
|
28535
|
-
if (language === "javascript" || language === "typescript") {
|
|
28872
|
+
if (language === "javascript" || language === "typescript" || language === "tsx") {
|
|
28536
28873
|
if (method === "random" && (receiver === "Math" || receiver.endsWith(".Math"))) {
|
|
28537
28874
|
return "Math.random";
|
|
28538
28875
|
}
|
|
@@ -29940,6 +30277,7 @@ function getNodeTypesForLanguage(language) {
|
|
|
29940
30277
|
]);
|
|
29941
30278
|
case "javascript":
|
|
29942
30279
|
case "typescript":
|
|
30280
|
+
case "tsx":
|
|
29943
30281
|
return /* @__PURE__ */ new Set([
|
|
29944
30282
|
"call_expression",
|
|
29945
30283
|
"new_expression",
|
|
@@ -29952,7 +30290,13 @@ function getNodeTypesForLanguage(language) {
|
|
|
29952
30290
|
"import_statement",
|
|
29953
30291
|
"export_statement",
|
|
29954
30292
|
"member_expression",
|
|
29955
|
-
"assignment_expression"
|
|
30293
|
+
"assignment_expression",
|
|
30294
|
+
// JSX node types — tree-sitter-tsx grammar (.tsx/.jsx routing)
|
|
30295
|
+
"jsx_element",
|
|
30296
|
+
"jsx_self_closing_element",
|
|
30297
|
+
"jsx_opening_element",
|
|
30298
|
+
"jsx_attribute",
|
|
30299
|
+
"jsx_expression"
|
|
29956
30300
|
]);
|
|
29957
30301
|
case "bash":
|
|
29958
30302
|
return /* @__PURE__ */ new Set([
|
|
@@ -30016,8 +30360,15 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
30016
30360
|
if (language === "html") {
|
|
30017
30361
|
return analyzeHtmlFile(code, filePath, options);
|
|
30018
30362
|
}
|
|
30019
|
-
|
|
30020
|
-
|
|
30363
|
+
let parseGrammar = language;
|
|
30364
|
+
if (language === "javascript" || language === "typescript") {
|
|
30365
|
+
const lower = filePath.toLowerCase();
|
|
30366
|
+
if (lower.endsWith(".tsx") || lower.endsWith(".jsx")) {
|
|
30367
|
+
parseGrammar = "tsx";
|
|
30368
|
+
}
|
|
30369
|
+
}
|
|
30370
|
+
logger.debug("Analyzing file", { filePath, language, parseGrammar, codeLength: code.length });
|
|
30371
|
+
const tree = await parse(code, parseGrammar);
|
|
30021
30372
|
try {
|
|
30022
30373
|
logger.trace("Parsed AST", { rootNodeType: tree.rootNode.type });
|
|
30023
30374
|
const parseStatus = extractParseStatus(tree);
|