circle-ir 3.48.0 → 3.49.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 +86 -2
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/constant-propagation/index.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/index.js +16 -6
- package/dist/analysis/constant-propagation/index.js.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.d.ts +53 -0
- package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -0
- package/dist/analysis/passes/insecure-cookie-pass.js +109 -0
- package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -0
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +7 -0
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +14 -0
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +50 -0
- 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 +21 -2
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +94 -3
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +117 -20
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +3 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +356 -26
- package/dist/core/circle-ir-core.cjs +189 -23
- package/dist/core/circle-ir-core.js +189 -23
- package/dist/core/extractors/types.js +85 -2
- package/dist/core/extractors/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -4732,14 +4732,46 @@ function extractJSClassInfo(node) {
|
|
|
4732
4732
|
end_line: node.endPosition.row + 1
|
|
4733
4733
|
};
|
|
4734
4734
|
}
|
|
4735
|
+
function extractDecoratorName(node) {
|
|
4736
|
+
const child = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
4737
|
+
if (!child) return null;
|
|
4738
|
+
if (child.type === "identifier") return getNodeText(child);
|
|
4739
|
+
if (child.type === "call_expression") {
|
|
4740
|
+
const fn = child.childForFieldName("function");
|
|
4741
|
+
if (fn) {
|
|
4742
|
+
if (fn.type === "identifier") return getNodeText(fn);
|
|
4743
|
+
if (fn.type === "member_expression") {
|
|
4744
|
+
const propNode = fn.childForFieldName("property");
|
|
4745
|
+
if (propNode) return getNodeText(propNode);
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
if (child.type === "member_expression") {
|
|
4750
|
+
const propNode = child.childForFieldName("property");
|
|
4751
|
+
if (propNode) return getNodeText(propNode);
|
|
4752
|
+
}
|
|
4753
|
+
return null;
|
|
4754
|
+
}
|
|
4735
4755
|
function extractJSMethods(body2) {
|
|
4736
4756
|
const methods = [];
|
|
4757
|
+
let pendingDecorators = [];
|
|
4737
4758
|
for (let i2 = 0; i2 < body2.childCount; i2++) {
|
|
4738
4759
|
const child = body2.child(i2);
|
|
4739
4760
|
if (!child) continue;
|
|
4761
|
+
if (child.type === "decorator") {
|
|
4762
|
+
const name2 = extractDecoratorName(child);
|
|
4763
|
+
if (name2) pendingDecorators.push(name2);
|
|
4764
|
+
continue;
|
|
4765
|
+
}
|
|
4766
|
+
if (child.type === "comment") continue;
|
|
4740
4767
|
if (child.type === "method_definition") {
|
|
4741
|
-
|
|
4768
|
+
const m = extractJSMethodInfo(child);
|
|
4769
|
+
if (pendingDecorators.length > 0) {
|
|
4770
|
+
m.annotations = pendingDecorators;
|
|
4771
|
+
}
|
|
4772
|
+
methods.push(m);
|
|
4742
4773
|
}
|
|
4774
|
+
pendingDecorators = [];
|
|
4743
4775
|
}
|
|
4744
4776
|
return methods;
|
|
4745
4777
|
}
|
|
@@ -4901,10 +4933,18 @@ function extractJSParameters(params) {
|
|
|
4901
4933
|
if (typeNode) {
|
|
4902
4934
|
paramType = getNodeText(typeNode).replace(/^:\s*/, "");
|
|
4903
4935
|
}
|
|
4936
|
+
const decorators = [];
|
|
4937
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
4938
|
+
const c = child.child(j);
|
|
4939
|
+
if (c && c.type === "decorator") {
|
|
4940
|
+
const name2 = extractDecoratorName(c);
|
|
4941
|
+
if (name2) decorators.push(name2);
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4904
4944
|
parameters.push({
|
|
4905
4945
|
name: paramName,
|
|
4906
4946
|
type: paramType,
|
|
4907
|
-
annotations:
|
|
4947
|
+
annotations: decorators,
|
|
4908
4948
|
line: child.startPosition.row + 1
|
|
4909
4949
|
});
|
|
4910
4950
|
}
|
|
@@ -10951,6 +10991,26 @@ var DEFAULT_SINKS = [
|
|
|
10951
10991
|
{ method: "setQuotedArgumentsEnabled", class: "Shell", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0] },
|
|
10952
10992
|
// Sandbox/script security
|
|
10953
10993
|
{ method: "onNewInstance", class: "SandboxInterceptor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10994
|
+
// Java Log Injection (slf4j / logback / java.util.logging) — CWE-117
|
|
10995
|
+
// Issue #44: log.info/warn/error/debug emit the message argument and any
|
|
10996
|
+
// {} format arguments to the log stream. Untrusted input forwarded into
|
|
10997
|
+
// these calls allows log forging (newline injection) and downstream log
|
|
10998
|
+
// analyzer pollution. Scoped to `java` so the generic method names don't
|
|
10999
|
+
// collide with JS console / Python logger entries below.
|
|
11000
|
+
{ method: "info", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
|
|
11001
|
+
{ method: "warn", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
|
|
11002
|
+
{ method: "error", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
|
|
11003
|
+
{ method: "debug", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
|
|
11004
|
+
{ method: "trace", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
|
|
11005
|
+
// java.util.logging.Logger uses the same class name `Logger` — same entries above cover it.
|
|
11006
|
+
// Severity-tagged levels: SEVERE/WARNING/INFO/CONFIG/FINE/FINER/FINEST
|
|
11007
|
+
{ method: "severe", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11008
|
+
{ method: "warning", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11009
|
+
{ method: "config", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11010
|
+
{ method: "fine", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11011
|
+
{ method: "finer", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11012
|
+
{ method: "finest", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11013
|
+
{ method: "log", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [1, 2, 3], languages: ["java"] },
|
|
10954
11014
|
// =========================================================================
|
|
10955
11015
|
// Node.js/Express Sinks
|
|
10956
11016
|
// =========================================================================
|
|
@@ -11003,13 +11063,47 @@ var DEFAULT_SINKS = [
|
|
|
11003
11063
|
{ method: "runInContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
11004
11064
|
{ method: "runInNewContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
11005
11065
|
{ method: "runInThisContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
11006
|
-
// Node.js NoSQL Injection (MongoDB)
|
|
11066
|
+
// Node.js NoSQL Injection (MongoDB native driver + mongoose) — CWE-943
|
|
11067
|
+
// Issue #45: the bare `class: 'Collection'` constraint missed mongoose's
|
|
11068
|
+
// fluent chains (mongoose.connection.db.collection('x').find({...})) and
|
|
11069
|
+
// Model.find calls because the call-site receiver type does not resolve
|
|
11070
|
+
// to `Collection`. Add classless+language-scoped entries for the
|
|
11071
|
+
// MongoDB-specific method names (findOne/aggregate/updateOne/etc.) and
|
|
11072
|
+
// mongoose `Model`/`Query` class entries. Bare `find` stays class-scoped
|
|
11073
|
+
// to avoid colliding with Array.prototype.find.
|
|
11007
11074
|
{ method: "find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11008
11075
|
{ method: "findOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11009
11076
|
{ method: "updateOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11010
11077
|
{ method: "updateMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11011
11078
|
{ method: "deleteOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11012
11079
|
{ method: "deleteMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11080
|
+
// Mongoose Model/Query class entries — Model.find/findOne/etc.
|
|
11081
|
+
{ method: "find", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11082
|
+
{ method: "findOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11083
|
+
{ method: "findById", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11084
|
+
{ method: "findOneAndUpdate", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11085
|
+
{ method: "findOneAndDelete", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11086
|
+
{ method: "findOneAndReplace", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11087
|
+
{ method: "updateOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11088
|
+
{ method: "updateMany", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11089
|
+
{ method: "deleteOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11090
|
+
{ method: "deleteMany", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11091
|
+
{ method: "countDocuments", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11092
|
+
{ method: "aggregate", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11093
|
+
// Mongoose Query class entries — chain methods returning Query
|
|
11094
|
+
{ method: "where", class: "Query", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11095
|
+
{ method: "equals", class: "Query", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11096
|
+
// Classless MongoDB-specific method names (rare outside MongoDB APIs) —
|
|
11097
|
+
// language-scoped to JS/TS. Excludes plain `find` (Array.prototype.find FP).
|
|
11098
|
+
{ method: "findOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11099
|
+
{ method: "findOneAndUpdate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11100
|
+
{ method: "findOneAndDelete", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11101
|
+
{ method: "findOneAndReplace", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11102
|
+
{ method: "updateOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11103
|
+
{ method: "updateMany", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11104
|
+
{ method: "deleteOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11105
|
+
{ method: "deleteMany", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11106
|
+
{ method: "aggregate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11013
11107
|
// Node.js SSRF (HTTP clients)
|
|
11014
11108
|
{ method: "get", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
11015
11109
|
{ method: "post", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
@@ -11031,6 +11125,24 @@ var DEFAULT_SINKS = [
|
|
|
11031
11125
|
{ method: "post", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
11032
11126
|
// node-fetch
|
|
11033
11127
|
{ method: "default", class: "node-fetch", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
11128
|
+
// Node.js / JavaScript Log Injection (console.*) — CWE-117
|
|
11129
|
+
// Issue #44: console.log/warn/error/info with tainted template literals
|
|
11130
|
+
// allow log forging (newline-injection) and downstream log analyzer
|
|
11131
|
+
// pollution. Scoped to JS/TS so the bare class `console` doesn't collide
|
|
11132
|
+
// with Python `console` module or Java identifiers.
|
|
11133
|
+
{ method: "log", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11134
|
+
{ method: "warn", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11135
|
+
{ method: "error", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11136
|
+
{ method: "info", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11137
|
+
{ method: "debug", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11138
|
+
{ method: "trace", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11139
|
+
// Node.js / Express Open Redirect — CWE-601
|
|
11140
|
+
// Issue #46: `res.redirect(req.query.next)` did not fire because the
|
|
11141
|
+
// legacy `class: 'Response'` constraint depended on receiver type
|
|
11142
|
+
// resolution of the Express `res` parameter. Mirror Python's classless
|
|
11143
|
+
// pattern with a language-scoped classless entry. The method name
|
|
11144
|
+
// `redirect` is rare outside HTTP frameworks so the FP risk is low.
|
|
11145
|
+
{ method: "redirect", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11034
11146
|
// =========================================================================
|
|
11035
11147
|
// Python Sinks
|
|
11036
11148
|
// =========================================================================
|
|
@@ -11072,7 +11184,12 @@ var DEFAULT_SINKS = [
|
|
|
11072
11184
|
{ method: "rmtree", class: "shutil", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0] },
|
|
11073
11185
|
{ method: "send_file", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
|
|
11074
11186
|
// Python XSS / SSTI
|
|
11075
|
-
|
|
11187
|
+
// Issue #54: Flask's `render_template_string(template_str)` with an
|
|
11188
|
+
// attacker-controlled template string is Server-Side Template Injection
|
|
11189
|
+
// (Jinja2 SSTI → RCE), not reflected XSS. Classify as code_injection
|
|
11190
|
+
// (CWE-94) with critical severity to match `jinja2.Template().render()`
|
|
11191
|
+
// and `Template.from_string()` entries above.
|
|
11192
|
+
{ method: "render_template_string", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["python"] },
|
|
11076
11193
|
{ method: "Markup", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
|
|
11077
11194
|
{ method: "mark_safe", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
|
|
11078
11195
|
// Python SSRF
|
|
@@ -11435,6 +11552,13 @@ var DEFAULT_SANITIZERS = [
|
|
|
11435
11552
|
{ method: "secure_filename", class: "werkzeug.utils", removes: ["path_traversal"] },
|
|
11436
11553
|
{ method: "basename", class: "os.path", removes: ["path_traversal"] },
|
|
11437
11554
|
{ method: "normpath", class: "os.path", removes: ["path_traversal"] },
|
|
11555
|
+
// Issue #48 part 2: realpath/abspath are canonical Python path-canonicalization
|
|
11556
|
+
// functions (analogous to Java File.getCanonicalPath). Register on both
|
|
11557
|
+
// `os.path` and the bare `path` receiver to cover `import os.path as path`.
|
|
11558
|
+
{ method: "realpath", class: "os.path", removes: ["path_traversal"] },
|
|
11559
|
+
{ method: "abspath", class: "os.path", removes: ["path_traversal"] },
|
|
11560
|
+
{ method: "realpath", class: "path", removes: ["path_traversal"] },
|
|
11561
|
+
{ method: "abspath", class: "path", removes: ["path_traversal"] },
|
|
11438
11562
|
// Python Type coercion
|
|
11439
11563
|
{ method: "int", removes: ["sql_injection", "command_injection", "xss"] },
|
|
11440
11564
|
{ method: "float", removes: ["sql_injection", "command_injection"] },
|
|
@@ -11496,7 +11620,7 @@ var PYTHON_TAINTED_PATTERNS = [
|
|
|
11496
11620
|
];
|
|
11497
11621
|
function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
|
|
11498
11622
|
const sourceLines = code !== void 0 ? code.split("\n") : void 0;
|
|
11499
|
-
const sources = findSources(calls, types, config.sources, sourceLines);
|
|
11623
|
+
const sources = findSources(calls, types, config.sources, sourceLines, language);
|
|
11500
11624
|
const sinks = findSinks(calls, config.sinks, typeHierarchy, language, sourceLines);
|
|
11501
11625
|
const sanitizers = findSanitizers(calls, types, config.sanitizers);
|
|
11502
11626
|
return { sources, sinks, sanitizers };
|
|
@@ -11514,7 +11638,7 @@ function attachSourceLineCode(sources, sinks, code) {
|
|
|
11514
11638
|
}
|
|
11515
11639
|
}
|
|
11516
11640
|
}
|
|
11517
|
-
function findSources(calls, types, patterns, sourceLines) {
|
|
11641
|
+
function findSources(calls, types, patterns, sourceLines, language) {
|
|
11518
11642
|
const sources = [];
|
|
11519
11643
|
for (const call of calls) {
|
|
11520
11644
|
for (const pattern of patterns) {
|
|
@@ -11567,23 +11691,29 @@ function findSources(calls, types, patterns, sourceLines) {
|
|
|
11567
11691
|
}
|
|
11568
11692
|
}
|
|
11569
11693
|
}
|
|
11570
|
-
const
|
|
11694
|
+
const RUST_EXTRACTOR_KIND = /(?:^|::)(Json|Form|Query|Path|Extension|Multipart|Body|Bytes)(?:<|$)/;
|
|
11571
11695
|
for (const type of types) {
|
|
11572
11696
|
for (const method of type.methods) {
|
|
11573
11697
|
for (const param of method.parameters) {
|
|
11574
|
-
if (param.type
|
|
11575
|
-
|
|
11576
|
-
|
|
11577
|
-
|
|
11578
|
-
|
|
11579
|
-
|
|
11580
|
-
|
|
11581
|
-
|
|
11582
|
-
|
|
11583
|
-
|
|
11584
|
-
|
|
11585
|
-
|
|
11586
|
-
|
|
11698
|
+
if (!param.type) continue;
|
|
11699
|
+
const kindMatch = RUST_EXTRACTOR_KIND.exec(param.type);
|
|
11700
|
+
if (!kindMatch) continue;
|
|
11701
|
+
const kind = kindMatch[1];
|
|
11702
|
+
if (kind === "Extension") continue;
|
|
11703
|
+
const sourceType = kind === "Form" || kind === "Query" || kind === "Path" ? "http_param" : "http_body";
|
|
11704
|
+
const paramLine = param.line ?? method.start_line;
|
|
11705
|
+
const alreadyExists = sources.some(
|
|
11706
|
+
(s) => s.line === paramLine && s.variable === param.name
|
|
11707
|
+
);
|
|
11708
|
+
if (alreadyExists) continue;
|
|
11709
|
+
sources.push({
|
|
11710
|
+
type: sourceType,
|
|
11711
|
+
location: `${param.type} ${param.name} in ${method.name}`,
|
|
11712
|
+
severity: "high",
|
|
11713
|
+
line: paramLine,
|
|
11714
|
+
confidence: 1,
|
|
11715
|
+
variable: param.name
|
|
11716
|
+
});
|
|
11587
11717
|
}
|
|
11588
11718
|
}
|
|
11589
11719
|
}
|
|
@@ -11665,6 +11795,15 @@ function findSources(calls, types, patterns, sourceLines) {
|
|
|
11665
11795
|
s.code = sourceLines[s.line - 1]?.trim();
|
|
11666
11796
|
}
|
|
11667
11797
|
}
|
|
11798
|
+
if (language === "rust" && sourceLines) {
|
|
11799
|
+
const LET_BINDING = /^\s*let\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::\s*[^=]+)?=/;
|
|
11800
|
+
for (const s of result) {
|
|
11801
|
+
if (s.variable && s.variable.length > 0) continue;
|
|
11802
|
+
const lineText = sourceLines[s.line - 1] ?? "";
|
|
11803
|
+
const m = LET_BINDING.exec(lineText);
|
|
11804
|
+
if (m) s.variable = m[1];
|
|
11805
|
+
}
|
|
11806
|
+
}
|
|
11668
11807
|
return result;
|
|
11669
11808
|
}
|
|
11670
11809
|
function isInterproceduralTaintableType(typeName) {
|
|
@@ -11770,6 +11909,20 @@ function isParameterizedQueryCall(call, pattern) {
|
|
|
11770
11909
|
}
|
|
11771
11910
|
return false;
|
|
11772
11911
|
}
|
|
11912
|
+
function isSafePythonSubprocessCall(call, pattern, language) {
|
|
11913
|
+
if (language !== "python") return false;
|
|
11914
|
+
if (pattern.type !== "command_injection") return false;
|
|
11915
|
+
if (pattern.class !== "subprocess") return false;
|
|
11916
|
+
const arg0 = call.arguments.find((a) => a.position === 0);
|
|
11917
|
+
if (!arg0) return false;
|
|
11918
|
+
const expr0 = (arg0.literal ?? arg0.expression ?? "").trim();
|
|
11919
|
+
if (!expr0.startsWith("[")) return false;
|
|
11920
|
+
for (const a of call.arguments) {
|
|
11921
|
+
const e = (a.expression ?? "").trim();
|
|
11922
|
+
if (/^shell\s*=\s*True\b/.test(e)) return false;
|
|
11923
|
+
}
|
|
11924
|
+
return true;
|
|
11925
|
+
}
|
|
11773
11926
|
var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
|
|
11774
11927
|
function argIsClassLiteral(call, position) {
|
|
11775
11928
|
const arg = call.arguments.find((a) => a.position === position);
|
|
@@ -11786,6 +11939,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
|
|
|
11786
11939
|
if (isParameterizedQueryCall(call, pattern)) {
|
|
11787
11940
|
continue;
|
|
11788
11941
|
}
|
|
11942
|
+
if (isSafePythonSubprocessCall(call, pattern, language)) {
|
|
11943
|
+
continue;
|
|
11944
|
+
}
|
|
11789
11945
|
if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
|
|
11790
11946
|
continue;
|
|
11791
11947
|
}
|
|
@@ -12188,7 +12344,12 @@ function receiverMightBeClass(receiver, className) {
|
|
|
12188
12344
|
"controller",
|
|
12189
12345
|
"task",
|
|
12190
12346
|
"thread",
|
|
12191
|
-
"job"
|
|
12347
|
+
"job",
|
|
12348
|
+
// Short Python DB abbreviation; would otherwise prefix-match obscure XSS
|
|
12349
|
+
// sink classes like XWiki's `CurrentTimePlugin` ('current'.startsWith('cur'))
|
|
12350
|
+
// via the CamelCase word prefix heuristic and produce an xss FP on every
|
|
12351
|
+
// `cur.execute(...)`. Resolved via commonMappings → ['Cursor']. See #65 / #48 pt3.
|
|
12352
|
+
"cur"
|
|
12192
12353
|
]);
|
|
12193
12354
|
const isAmbiguous = ambiguousIdentifiers.has(lowerReceiver);
|
|
12194
12355
|
if (!isAmbiguous && lowerReceiver.length >= 3 && lowerClass.includes(lowerReceiver)) {
|
|
@@ -12198,7 +12359,9 @@ function receiverMightBeClass(receiver, className) {
|
|
|
12198
12359
|
}
|
|
12199
12360
|
if (!isAmbiguous && lowerReceiver.length >= 2) {
|
|
12200
12361
|
if (lowerClass.startsWith(lowerReceiver) || lowerClass.endsWith(lowerReceiver)) {
|
|
12201
|
-
|
|
12362
|
+
if (lowerReceiver.length / lowerClass.length >= 0.4) {
|
|
12363
|
+
return true;
|
|
12364
|
+
}
|
|
12202
12365
|
}
|
|
12203
12366
|
}
|
|
12204
12367
|
if (!isAmbiguous && lowerReceiver.length >= 3) {
|
|
@@ -12221,6 +12384,9 @@ function receiverMightBeClass(receiver, className) {
|
|
|
12221
12384
|
ps: ["PreparedStatement"],
|
|
12222
12385
|
rs: ["ResultSet"],
|
|
12223
12386
|
template: ["JdbcTemplate"],
|
|
12387
|
+
cur: ["Cursor"],
|
|
12388
|
+
// Python DB-API cursor — see ambiguousIdentifiers note
|
|
12389
|
+
cursor: ["Cursor"],
|
|
12224
12390
|
// I/O
|
|
12225
12391
|
writer: ["PrintWriter"],
|
|
12226
12392
|
out: ["PrintWriter", "OutputStream"],
|
|
@@ -15268,7 +15434,7 @@ function isFalsePositive(result, sinkLine, taintedVar) {
|
|
|
15268
15434
|
if (varValue && varValue.type !== "unknown" && !result.tainted.has(taintedVar)) {
|
|
15269
15435
|
return { isFalsePositive: true, reason: `variable_is_constant: ${varValue.value}` };
|
|
15270
15436
|
}
|
|
15271
|
-
if (result.symbols.
|
|
15437
|
+
if (result.symbols.has(taintedVar) && !result.tainted.has(taintedVar)) {
|
|
15272
15438
|
return { isFalsePositive: true, reason: "variable_not_tainted" };
|
|
15273
15439
|
}
|
|
15274
15440
|
return { isFalsePositive: false, reason: null };
|
|
@@ -560,18 +560,90 @@ function extractJSClassInfo(node) {
|
|
|
560
560
|
end_line: node.endPosition.row + 1,
|
|
561
561
|
};
|
|
562
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* Extract the name of a `decorator` node (TypeScript / NestJS / Angular etc).
|
|
565
|
+
*
|
|
566
|
+
* The grammar permits three shapes:
|
|
567
|
+
* @Foo → decorator > identifier
|
|
568
|
+
* @Foo('bar') → decorator > call_expression > identifier
|
|
569
|
+
* @ns.Foo → decorator > member_expression (use .property)
|
|
570
|
+
* @ns.Foo('bar') → decorator > call_expression > member_expression
|
|
571
|
+
*/
|
|
572
|
+
function extractDecoratorName(node) {
|
|
573
|
+
// tree-sitter-typescript stores the decorator expression as namedChild(0)
|
|
574
|
+
const child = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
575
|
+
if (!child)
|
|
576
|
+
return null;
|
|
577
|
+
if (child.type === 'identifier')
|
|
578
|
+
return getNodeText(child);
|
|
579
|
+
if (child.type === 'call_expression') {
|
|
580
|
+
const fn = child.childForFieldName('function');
|
|
581
|
+
if (fn) {
|
|
582
|
+
if (fn.type === 'identifier')
|
|
583
|
+
return getNodeText(fn);
|
|
584
|
+
if (fn.type === 'member_expression') {
|
|
585
|
+
const propNode = fn.childForFieldName('property');
|
|
586
|
+
if (propNode)
|
|
587
|
+
return getNodeText(propNode);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (child.type === 'member_expression') {
|
|
592
|
+
const propNode = child.childForFieldName('property');
|
|
593
|
+
if (propNode)
|
|
594
|
+
return getNodeText(propNode);
|
|
595
|
+
}
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
563
598
|
/**
|
|
564
599
|
* Extract JavaScript methods from a class body.
|
|
600
|
+
*
|
|
601
|
+
* Method-level decorators (TS / NestJS / Angular) appear as preceding
|
|
602
|
+
* `decorator` siblings of `method_definition` inside `class_body`. We accumulate
|
|
603
|
+
* them as we walk children and attach to the very next `method_definition`.
|
|
604
|
+
*
|
|
605
|
+
* IMPORTANT: `pendingDecorators` is reset on ANY non-decorator class member
|
|
606
|
+
* (field, accessor, abstract signature, …), not just method_definition. A
|
|
607
|
+
* decorator preceding a field like
|
|
608
|
+
*
|
|
609
|
+
* @Inject('USER_REPO') private repo: Repository<User>;
|
|
610
|
+
* @Get('search') async search(...) { ... }
|
|
611
|
+
*
|
|
612
|
+
* belongs to the field, not the method below. Failing to reset after the
|
|
613
|
+
* field_definition would silently transfer `Inject` onto `search.annotations`,
|
|
614
|
+
* polluting the IR consumed by taint-matcher.ts (`matchesAnnotation` against
|
|
615
|
+
* `pattern.method_annotation`).
|
|
565
616
|
*/
|
|
566
617
|
function extractJSMethods(body) {
|
|
567
618
|
const methods = [];
|
|
619
|
+
let pendingDecorators = [];
|
|
568
620
|
for (let i = 0; i < body.childCount; i++) {
|
|
569
621
|
const child = body.child(i);
|
|
570
622
|
if (!child)
|
|
571
623
|
continue;
|
|
624
|
+
if (child.type === 'decorator') {
|
|
625
|
+
const name = extractDecoratorName(child);
|
|
626
|
+
if (name)
|
|
627
|
+
pendingDecorators.push(name);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
// Tree-sitter emits comments as anonymous children of `class_body`.
|
|
631
|
+
// A `// note` line between a decorator and its method must NOT clear
|
|
632
|
+
// the pending decorator list — skip without resetting.
|
|
633
|
+
if (child.type === 'comment')
|
|
634
|
+
continue;
|
|
572
635
|
if (child.type === 'method_definition') {
|
|
573
|
-
|
|
636
|
+
const m = extractJSMethodInfo(child);
|
|
637
|
+
if (pendingDecorators.length > 0) {
|
|
638
|
+
m.annotations = pendingDecorators;
|
|
639
|
+
}
|
|
640
|
+
methods.push(m);
|
|
574
641
|
}
|
|
642
|
+
// Reset pending decorators on ANY non-decorator, non-comment child
|
|
643
|
+
// (method, field, accessor, abstract_method_signature,
|
|
644
|
+
// public_field_definition, …). Decorators only ever apply to the
|
|
645
|
+
// immediately-following member.
|
|
646
|
+
pendingDecorators = [];
|
|
575
647
|
}
|
|
576
648
|
return methods;
|
|
577
649
|
}
|
|
@@ -776,10 +848,21 @@ function extractJSParameters(params) {
|
|
|
776
848
|
// type_annotation includes the leading ':'; strip it for storage parity with other languages
|
|
777
849
|
paramType = getNodeText(typeNode).replace(/^:\s*/, '');
|
|
778
850
|
}
|
|
851
|
+
// Parameter decorators (NestJS @Query/@Param/@Body, Angular @Inject, etc.)
|
|
852
|
+
// appear as `decorator` children of the required_parameter node itself.
|
|
853
|
+
const decorators = [];
|
|
854
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
855
|
+
const c = child.child(j);
|
|
856
|
+
if (c && c.type === 'decorator') {
|
|
857
|
+
const name = extractDecoratorName(c);
|
|
858
|
+
if (name)
|
|
859
|
+
decorators.push(name);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
779
862
|
parameters.push({
|
|
780
863
|
name: paramName,
|
|
781
864
|
type: paramType,
|
|
782
|
-
annotations:
|
|
865
|
+
annotations: decorators,
|
|
783
866
|
line: child.startPosition.row + 1,
|
|
784
867
|
});
|
|
785
868
|
}
|