circle-ir 3.47.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/constant-propagation/propagator.d.ts +7 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +81 -41
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/html/html-attribute-security-pass.js +14 -9
- package/dist/analysis/html/html-attribute-security-pass.js.map +1 -1
- package/dist/analysis/html/html-extractor.d.ts.map +1 -1
- package/dist/analysis/html/html-extractor.js +16 -11
- package/dist/analysis/html/html-extractor.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 +453 -99
- package/dist/core/circle-ir-core.cjs +251 -64
- package/dist/core/circle-ir-core.js +251 -64
- package/dist/core/extractors/types.js +85 -2
- package/dist/core/extractors/types.js.map +1 -1
- package/dist/core/parser.d.ts +10 -0
- package/dist/core/parser.d.ts.map +1 -1
- package/dist/core/parser.js +20 -5
- package/dist/core/parser.js.map +1 -1
- package/dist/languages/plugins/base.d.ts.map +1 -1
- package/dist/languages/plugins/base.js +15 -11
- package/dist/languages/plugins/base.js.map +1 -1
- package/dist/languages/plugins/java.d.ts.map +1 -1
- package/dist/languages/plugins/java.js +8 -4
- package/dist/languages/plugins/java.js.map +1 -1
- package/package.json +1 -1
|
@@ -4127,11 +4127,13 @@ function disposeTree(tree) {
|
|
|
4127
4127
|
}
|
|
4128
4128
|
}
|
|
4129
4129
|
function walkTree(node, visitor) {
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
const
|
|
4133
|
-
|
|
4134
|
-
|
|
4130
|
+
const stack = [node];
|
|
4131
|
+
while (stack.length > 0) {
|
|
4132
|
+
const current = stack.pop();
|
|
4133
|
+
visitor(current);
|
|
4134
|
+
for (let i2 = current.childCount - 1; i2 >= 0; i2--) {
|
|
4135
|
+
const child = current.child(i2);
|
|
4136
|
+
if (child) stack.push(child);
|
|
4135
4137
|
}
|
|
4136
4138
|
}
|
|
4137
4139
|
}
|
|
@@ -4750,14 +4752,46 @@ function extractJSClassInfo(node) {
|
|
|
4750
4752
|
end_line: node.endPosition.row + 1
|
|
4751
4753
|
};
|
|
4752
4754
|
}
|
|
4755
|
+
function extractDecoratorName(node) {
|
|
4756
|
+
const child = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
4757
|
+
if (!child) return null;
|
|
4758
|
+
if (child.type === "identifier") return getNodeText(child);
|
|
4759
|
+
if (child.type === "call_expression") {
|
|
4760
|
+
const fn = child.childForFieldName("function");
|
|
4761
|
+
if (fn) {
|
|
4762
|
+
if (fn.type === "identifier") return getNodeText(fn);
|
|
4763
|
+
if (fn.type === "member_expression") {
|
|
4764
|
+
const propNode = fn.childForFieldName("property");
|
|
4765
|
+
if (propNode) return getNodeText(propNode);
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
if (child.type === "member_expression") {
|
|
4770
|
+
const propNode = child.childForFieldName("property");
|
|
4771
|
+
if (propNode) return getNodeText(propNode);
|
|
4772
|
+
}
|
|
4773
|
+
return null;
|
|
4774
|
+
}
|
|
4753
4775
|
function extractJSMethods(body2) {
|
|
4754
4776
|
const methods = [];
|
|
4777
|
+
let pendingDecorators = [];
|
|
4755
4778
|
for (let i2 = 0; i2 < body2.childCount; i2++) {
|
|
4756
4779
|
const child = body2.child(i2);
|
|
4757
4780
|
if (!child) continue;
|
|
4781
|
+
if (child.type === "decorator") {
|
|
4782
|
+
const name2 = extractDecoratorName(child);
|
|
4783
|
+
if (name2) pendingDecorators.push(name2);
|
|
4784
|
+
continue;
|
|
4785
|
+
}
|
|
4786
|
+
if (child.type === "comment") continue;
|
|
4758
4787
|
if (child.type === "method_definition") {
|
|
4759
|
-
|
|
4788
|
+
const m = extractJSMethodInfo(child);
|
|
4789
|
+
if (pendingDecorators.length > 0) {
|
|
4790
|
+
m.annotations = pendingDecorators;
|
|
4791
|
+
}
|
|
4792
|
+
methods.push(m);
|
|
4760
4793
|
}
|
|
4794
|
+
pendingDecorators = [];
|
|
4761
4795
|
}
|
|
4762
4796
|
return methods;
|
|
4763
4797
|
}
|
|
@@ -4919,10 +4953,18 @@ function extractJSParameters(params) {
|
|
|
4919
4953
|
if (typeNode) {
|
|
4920
4954
|
paramType = getNodeText(typeNode).replace(/^:\s*/, "");
|
|
4921
4955
|
}
|
|
4956
|
+
const decorators = [];
|
|
4957
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
4958
|
+
const c = child.child(j);
|
|
4959
|
+
if (c && c.type === "decorator") {
|
|
4960
|
+
const name2 = extractDecoratorName(c);
|
|
4961
|
+
if (name2) decorators.push(name2);
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4922
4964
|
parameters.push({
|
|
4923
4965
|
name: paramName,
|
|
4924
4966
|
type: paramType,
|
|
4925
|
-
annotations:
|
|
4967
|
+
annotations: decorators,
|
|
4926
4968
|
line: child.startPosition.row + 1
|
|
4927
4969
|
});
|
|
4928
4970
|
}
|
|
@@ -11633,6 +11675,26 @@ var DEFAULT_SINKS = [
|
|
|
11633
11675
|
{ method: "setQuotedArgumentsEnabled", class: "Shell", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0] },
|
|
11634
11676
|
// Sandbox/script security
|
|
11635
11677
|
{ method: "onNewInstance", class: "SandboxInterceptor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11678
|
+
// Java Log Injection (slf4j / logback / java.util.logging) — CWE-117
|
|
11679
|
+
// Issue #44: log.info/warn/error/debug emit the message argument and any
|
|
11680
|
+
// {} format arguments to the log stream. Untrusted input forwarded into
|
|
11681
|
+
// these calls allows log forging (newline injection) and downstream log
|
|
11682
|
+
// analyzer pollution. Scoped to `java` so the generic method names don't
|
|
11683
|
+
// collide with JS console / Python logger entries below.
|
|
11684
|
+
{ method: "info", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
|
|
11685
|
+
{ method: "warn", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
|
|
11686
|
+
{ method: "error", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
|
|
11687
|
+
{ method: "debug", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
|
|
11688
|
+
{ method: "trace", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
|
|
11689
|
+
// java.util.logging.Logger uses the same class name `Logger` — same entries above cover it.
|
|
11690
|
+
// Severity-tagged levels: SEVERE/WARNING/INFO/CONFIG/FINE/FINER/FINEST
|
|
11691
|
+
{ method: "severe", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11692
|
+
{ method: "warning", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11693
|
+
{ method: "config", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11694
|
+
{ method: "fine", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11695
|
+
{ method: "finer", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11696
|
+
{ method: "finest", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11697
|
+
{ method: "log", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [1, 2, 3], languages: ["java"] },
|
|
11636
11698
|
// =========================================================================
|
|
11637
11699
|
// Node.js/Express Sinks
|
|
11638
11700
|
// =========================================================================
|
|
@@ -11685,13 +11747,47 @@ var DEFAULT_SINKS = [
|
|
|
11685
11747
|
{ method: "runInContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
11686
11748
|
{ method: "runInNewContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
11687
11749
|
{ method: "runInThisContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
11688
|
-
// Node.js NoSQL Injection (MongoDB)
|
|
11750
|
+
// Node.js NoSQL Injection (MongoDB native driver + mongoose) — CWE-943
|
|
11751
|
+
// Issue #45: the bare `class: 'Collection'` constraint missed mongoose's
|
|
11752
|
+
// fluent chains (mongoose.connection.db.collection('x').find({...})) and
|
|
11753
|
+
// Model.find calls because the call-site receiver type does not resolve
|
|
11754
|
+
// to `Collection`. Add classless+language-scoped entries for the
|
|
11755
|
+
// MongoDB-specific method names (findOne/aggregate/updateOne/etc.) and
|
|
11756
|
+
// mongoose `Model`/`Query` class entries. Bare `find` stays class-scoped
|
|
11757
|
+
// to avoid colliding with Array.prototype.find.
|
|
11689
11758
|
{ method: "find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11690
11759
|
{ method: "findOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11691
11760
|
{ method: "updateOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11692
11761
|
{ method: "updateMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11693
11762
|
{ method: "deleteOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11694
11763
|
{ method: "deleteMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11764
|
+
// Mongoose Model/Query class entries — Model.find/findOne/etc.
|
|
11765
|
+
{ method: "find", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11766
|
+
{ method: "findOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11767
|
+
{ method: "findById", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11768
|
+
{ method: "findOneAndUpdate", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11769
|
+
{ method: "findOneAndDelete", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11770
|
+
{ method: "findOneAndReplace", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11771
|
+
{ method: "updateOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11772
|
+
{ method: "updateMany", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11773
|
+
{ method: "deleteOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11774
|
+
{ method: "deleteMany", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11775
|
+
{ method: "countDocuments", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11776
|
+
{ method: "aggregate", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11777
|
+
// Mongoose Query class entries — chain methods returning Query
|
|
11778
|
+
{ method: "where", class: "Query", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11779
|
+
{ method: "equals", class: "Query", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11780
|
+
// Classless MongoDB-specific method names (rare outside MongoDB APIs) —
|
|
11781
|
+
// language-scoped to JS/TS. Excludes plain `find` (Array.prototype.find FP).
|
|
11782
|
+
{ method: "findOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11783
|
+
{ method: "findOneAndUpdate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11784
|
+
{ method: "findOneAndDelete", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11785
|
+
{ method: "findOneAndReplace", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11786
|
+
{ method: "updateOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11787
|
+
{ method: "updateMany", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11788
|
+
{ method: "deleteOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11789
|
+
{ method: "deleteMany", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11790
|
+
{ method: "aggregate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11695
11791
|
// Node.js SSRF (HTTP clients)
|
|
11696
11792
|
{ method: "get", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
11697
11793
|
{ method: "post", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
@@ -11713,6 +11809,24 @@ var DEFAULT_SINKS = [
|
|
|
11713
11809
|
{ method: "post", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
11714
11810
|
// node-fetch
|
|
11715
11811
|
{ method: "default", class: "node-fetch", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
11812
|
+
// Node.js / JavaScript Log Injection (console.*) — CWE-117
|
|
11813
|
+
// Issue #44: console.log/warn/error/info with tainted template literals
|
|
11814
|
+
// allow log forging (newline-injection) and downstream log analyzer
|
|
11815
|
+
// pollution. Scoped to JS/TS so the bare class `console` doesn't collide
|
|
11816
|
+
// with Python `console` module or Java identifiers.
|
|
11817
|
+
{ method: "log", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11818
|
+
{ method: "warn", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11819
|
+
{ method: "error", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11820
|
+
{ method: "info", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11821
|
+
{ method: "debug", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11822
|
+
{ method: "trace", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11823
|
+
// Node.js / Express Open Redirect — CWE-601
|
|
11824
|
+
// Issue #46: `res.redirect(req.query.next)` did not fire because the
|
|
11825
|
+
// legacy `class: 'Response'` constraint depended on receiver type
|
|
11826
|
+
// resolution of the Express `res` parameter. Mirror Python's classless
|
|
11827
|
+
// pattern with a language-scoped classless entry. The method name
|
|
11828
|
+
// `redirect` is rare outside HTTP frameworks so the FP risk is low.
|
|
11829
|
+
{ method: "redirect", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11716
11830
|
// =========================================================================
|
|
11717
11831
|
// Python Sinks
|
|
11718
11832
|
// =========================================================================
|
|
@@ -11754,7 +11868,12 @@ var DEFAULT_SINKS = [
|
|
|
11754
11868
|
{ method: "rmtree", class: "shutil", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0] },
|
|
11755
11869
|
{ method: "send_file", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
|
|
11756
11870
|
// Python XSS / SSTI
|
|
11757
|
-
|
|
11871
|
+
// Issue #54: Flask's `render_template_string(template_str)` with an
|
|
11872
|
+
// attacker-controlled template string is Server-Side Template Injection
|
|
11873
|
+
// (Jinja2 SSTI → RCE), not reflected XSS. Classify as code_injection
|
|
11874
|
+
// (CWE-94) with critical severity to match `jinja2.Template().render()`
|
|
11875
|
+
// and `Template.from_string()` entries above.
|
|
11876
|
+
{ method: "render_template_string", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["python"] },
|
|
11758
11877
|
{ method: "Markup", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
|
|
11759
11878
|
{ method: "mark_safe", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
|
|
11760
11879
|
// Python SSRF
|
|
@@ -12117,6 +12236,13 @@ var DEFAULT_SANITIZERS = [
|
|
|
12117
12236
|
{ method: "secure_filename", class: "werkzeug.utils", removes: ["path_traversal"] },
|
|
12118
12237
|
{ method: "basename", class: "os.path", removes: ["path_traversal"] },
|
|
12119
12238
|
{ method: "normpath", class: "os.path", removes: ["path_traversal"] },
|
|
12239
|
+
// Issue #48 part 2: realpath/abspath are canonical Python path-canonicalization
|
|
12240
|
+
// functions (analogous to Java File.getCanonicalPath). Register on both
|
|
12241
|
+
// `os.path` and the bare `path` receiver to cover `import os.path as path`.
|
|
12242
|
+
{ method: "realpath", class: "os.path", removes: ["path_traversal"] },
|
|
12243
|
+
{ method: "abspath", class: "os.path", removes: ["path_traversal"] },
|
|
12244
|
+
{ method: "realpath", class: "path", removes: ["path_traversal"] },
|
|
12245
|
+
{ method: "abspath", class: "path", removes: ["path_traversal"] },
|
|
12120
12246
|
// Python Type coercion
|
|
12121
12247
|
{ method: "int", removes: ["sql_injection", "command_injection", "xss"] },
|
|
12122
12248
|
{ method: "float", removes: ["sql_injection", "command_injection"] },
|
|
@@ -12265,7 +12391,7 @@ var PYTHON_TAINTED_PATTERNS = [
|
|
|
12265
12391
|
];
|
|
12266
12392
|
function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
|
|
12267
12393
|
const sourceLines = code !== void 0 ? code.split("\n") : void 0;
|
|
12268
|
-
const sources = findSources(calls, types, config.sources, sourceLines);
|
|
12394
|
+
const sources = findSources(calls, types, config.sources, sourceLines, language);
|
|
12269
12395
|
const sinks = findSinks(calls, config.sinks, typeHierarchy, language, sourceLines);
|
|
12270
12396
|
const sanitizers = findSanitizers(calls, types, config.sanitizers);
|
|
12271
12397
|
return { sources, sinks, sanitizers };
|
|
@@ -12283,7 +12409,7 @@ function attachSourceLineCode(sources, sinks, code) {
|
|
|
12283
12409
|
}
|
|
12284
12410
|
}
|
|
12285
12411
|
}
|
|
12286
|
-
function findSources(calls, types, patterns, sourceLines) {
|
|
12412
|
+
function findSources(calls, types, patterns, sourceLines, language) {
|
|
12287
12413
|
const sources = [];
|
|
12288
12414
|
for (const call of calls) {
|
|
12289
12415
|
for (const pattern of patterns) {
|
|
@@ -12336,23 +12462,29 @@ function findSources(calls, types, patterns, sourceLines) {
|
|
|
12336
12462
|
}
|
|
12337
12463
|
}
|
|
12338
12464
|
}
|
|
12339
|
-
const
|
|
12465
|
+
const RUST_EXTRACTOR_KIND = /(?:^|::)(Json|Form|Query|Path|Extension|Multipart|Body|Bytes)(?:<|$)/;
|
|
12340
12466
|
for (const type of types) {
|
|
12341
12467
|
for (const method of type.methods) {
|
|
12342
12468
|
for (const param of method.parameters) {
|
|
12343
|
-
if (param.type
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12469
|
+
if (!param.type) continue;
|
|
12470
|
+
const kindMatch = RUST_EXTRACTOR_KIND.exec(param.type);
|
|
12471
|
+
if (!kindMatch) continue;
|
|
12472
|
+
const kind = kindMatch[1];
|
|
12473
|
+
if (kind === "Extension") continue;
|
|
12474
|
+
const sourceType = kind === "Form" || kind === "Query" || kind === "Path" ? "http_param" : "http_body";
|
|
12475
|
+
const paramLine = param.line ?? method.start_line;
|
|
12476
|
+
const alreadyExists = sources.some(
|
|
12477
|
+
(s) => s.line === paramLine && s.variable === param.name
|
|
12478
|
+
);
|
|
12479
|
+
if (alreadyExists) continue;
|
|
12480
|
+
sources.push({
|
|
12481
|
+
type: sourceType,
|
|
12482
|
+
location: `${param.type} ${param.name} in ${method.name}`,
|
|
12483
|
+
severity: "high",
|
|
12484
|
+
line: paramLine,
|
|
12485
|
+
confidence: 1,
|
|
12486
|
+
variable: param.name
|
|
12487
|
+
});
|
|
12356
12488
|
}
|
|
12357
12489
|
}
|
|
12358
12490
|
}
|
|
@@ -12434,6 +12566,15 @@ function findSources(calls, types, patterns, sourceLines) {
|
|
|
12434
12566
|
s.code = sourceLines[s.line - 1]?.trim();
|
|
12435
12567
|
}
|
|
12436
12568
|
}
|
|
12569
|
+
if (language === "rust" && sourceLines) {
|
|
12570
|
+
const LET_BINDING = /^\s*let\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::\s*[^=]+)?=/;
|
|
12571
|
+
for (const s of result) {
|
|
12572
|
+
if (s.variable && s.variable.length > 0) continue;
|
|
12573
|
+
const lineText = sourceLines[s.line - 1] ?? "";
|
|
12574
|
+
const m = LET_BINDING.exec(lineText);
|
|
12575
|
+
if (m) s.variable = m[1];
|
|
12576
|
+
}
|
|
12577
|
+
}
|
|
12437
12578
|
return result;
|
|
12438
12579
|
}
|
|
12439
12580
|
function isInterproceduralTaintableType(typeName) {
|
|
@@ -12539,6 +12680,20 @@ function isParameterizedQueryCall(call, pattern) {
|
|
|
12539
12680
|
}
|
|
12540
12681
|
return false;
|
|
12541
12682
|
}
|
|
12683
|
+
function isSafePythonSubprocessCall(call, pattern, language) {
|
|
12684
|
+
if (language !== "python") return false;
|
|
12685
|
+
if (pattern.type !== "command_injection") return false;
|
|
12686
|
+
if (pattern.class !== "subprocess") return false;
|
|
12687
|
+
const arg0 = call.arguments.find((a) => a.position === 0);
|
|
12688
|
+
if (!arg0) return false;
|
|
12689
|
+
const expr0 = (arg0.literal ?? arg0.expression ?? "").trim();
|
|
12690
|
+
if (!expr0.startsWith("[")) return false;
|
|
12691
|
+
for (const a of call.arguments) {
|
|
12692
|
+
const e = (a.expression ?? "").trim();
|
|
12693
|
+
if (/^shell\s*=\s*True\b/.test(e)) return false;
|
|
12694
|
+
}
|
|
12695
|
+
return true;
|
|
12696
|
+
}
|
|
12542
12697
|
var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
|
|
12543
12698
|
function argIsClassLiteral(call, position) {
|
|
12544
12699
|
const arg = call.arguments.find((a) => a.position === position);
|
|
@@ -12555,6 +12710,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
|
|
|
12555
12710
|
if (isParameterizedQueryCall(call, pattern)) {
|
|
12556
12711
|
continue;
|
|
12557
12712
|
}
|
|
12713
|
+
if (isSafePythonSubprocessCall(call, pattern, language)) {
|
|
12714
|
+
continue;
|
|
12715
|
+
}
|
|
12558
12716
|
if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
|
|
12559
12717
|
continue;
|
|
12560
12718
|
}
|
|
@@ -12957,7 +13115,12 @@ function receiverMightBeClass(receiver, className) {
|
|
|
12957
13115
|
"controller",
|
|
12958
13116
|
"task",
|
|
12959
13117
|
"thread",
|
|
12960
|
-
"job"
|
|
13118
|
+
"job",
|
|
13119
|
+
// Short Python DB abbreviation; would otherwise prefix-match obscure XSS
|
|
13120
|
+
// sink classes like XWiki's `CurrentTimePlugin` ('current'.startsWith('cur'))
|
|
13121
|
+
// via the CamelCase word prefix heuristic and produce an xss FP on every
|
|
13122
|
+
// `cur.execute(...)`. Resolved via commonMappings → ['Cursor']. See #65 / #48 pt3.
|
|
13123
|
+
"cur"
|
|
12961
13124
|
]);
|
|
12962
13125
|
const isAmbiguous = ambiguousIdentifiers.has(lowerReceiver);
|
|
12963
13126
|
if (!isAmbiguous && lowerReceiver.length >= 3 && lowerClass.includes(lowerReceiver)) {
|
|
@@ -12967,7 +13130,9 @@ function receiverMightBeClass(receiver, className) {
|
|
|
12967
13130
|
}
|
|
12968
13131
|
if (!isAmbiguous && lowerReceiver.length >= 2) {
|
|
12969
13132
|
if (lowerClass.startsWith(lowerReceiver) || lowerClass.endsWith(lowerReceiver)) {
|
|
12970
|
-
|
|
13133
|
+
if (lowerReceiver.length / lowerClass.length >= 0.4) {
|
|
13134
|
+
return true;
|
|
13135
|
+
}
|
|
12971
13136
|
}
|
|
12972
13137
|
}
|
|
12973
13138
|
if (!isAmbiguous && lowerReceiver.length >= 3) {
|
|
@@ -12990,6 +13155,9 @@ function receiverMightBeClass(receiver, className) {
|
|
|
12990
13155
|
ps: ["PreparedStatement"],
|
|
12991
13156
|
rs: ["ResultSet"],
|
|
12992
13157
|
template: ["JdbcTemplate"],
|
|
13158
|
+
cur: ["Cursor"],
|
|
13159
|
+
// Python DB-API cursor — see ambiguousIdentifiers note
|
|
13160
|
+
cursor: ["Cursor"],
|
|
12993
13161
|
// I/O
|
|
12994
13162
|
writer: ["PrintWriter"],
|
|
12995
13163
|
out: ["PrintWriter", "OutputStream"],
|
|
@@ -16032,8 +16200,10 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
16032
16200
|
* These are variables declared directly in the class body, not inside methods.
|
|
16033
16201
|
*/
|
|
16034
16202
|
collectClassFields(root) {
|
|
16035
|
-
const
|
|
16036
|
-
|
|
16203
|
+
const stack = [root];
|
|
16204
|
+
while (stack.length > 0) {
|
|
16205
|
+
const n = stack.pop();
|
|
16206
|
+
if (!n) continue;
|
|
16037
16207
|
if (n.type === "class_body") {
|
|
16038
16208
|
for (const child of n.children) {
|
|
16039
16209
|
if (child.type === "field_declaration") {
|
|
@@ -16047,32 +16217,28 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
16047
16217
|
}
|
|
16048
16218
|
}
|
|
16049
16219
|
}
|
|
16050
|
-
|
|
16051
|
-
traverse(child, true, true);
|
|
16052
|
-
} else {
|
|
16053
|
-
traverse(child, true, false);
|
|
16054
|
-
}
|
|
16220
|
+
stack.push(child);
|
|
16055
16221
|
}
|
|
16056
|
-
|
|
16222
|
+
continue;
|
|
16057
16223
|
}
|
|
16058
16224
|
for (const child of n.children) {
|
|
16059
|
-
|
|
16225
|
+
stack.push(child);
|
|
16060
16226
|
}
|
|
16061
|
-
}
|
|
16062
|
-
traverse(root, false, false);
|
|
16227
|
+
}
|
|
16063
16228
|
}
|
|
16064
16229
|
findAllMethods(node) {
|
|
16065
16230
|
const methods = [];
|
|
16066
|
-
const
|
|
16067
|
-
|
|
16231
|
+
const stack = [node];
|
|
16232
|
+
while (stack.length > 0) {
|
|
16233
|
+
const n = stack.pop();
|
|
16234
|
+
if (!n) continue;
|
|
16068
16235
|
if (n.type === "method_declaration" || n.type === "function_declaration") {
|
|
16069
16236
|
methods.push(n);
|
|
16070
16237
|
}
|
|
16071
16238
|
for (const child of n.children) {
|
|
16072
|
-
if (child)
|
|
16239
|
+
if (child) stack.push(child);
|
|
16073
16240
|
}
|
|
16074
|
-
}
|
|
16075
|
-
traverse(node);
|
|
16241
|
+
}
|
|
16076
16242
|
return methods;
|
|
16077
16243
|
}
|
|
16078
16244
|
getMethodName(method) {
|
|
@@ -16123,9 +16289,24 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
16123
16289
|
// AST Visitor
|
|
16124
16290
|
// ===========================================================================
|
|
16125
16291
|
visit(node) {
|
|
16292
|
+
const stack = [node];
|
|
16293
|
+
while (stack.length > 0) {
|
|
16294
|
+
const current = stack.pop();
|
|
16295
|
+
if (this.visitOne(current)) continue;
|
|
16296
|
+
for (let i2 = current.children.length - 1; i2 >= 0; i2--) {
|
|
16297
|
+
stack.push(current.children[i2]);
|
|
16298
|
+
}
|
|
16299
|
+
}
|
|
16300
|
+
}
|
|
16301
|
+
/**
|
|
16302
|
+
* Visit a single node. Returns true if the handler already descended into
|
|
16303
|
+
* children (and the caller should NOT push them), false to fall through to
|
|
16304
|
+
* the default pre-order descent.
|
|
16305
|
+
*/
|
|
16306
|
+
visitOne(node) {
|
|
16126
16307
|
const line = getNodeLine(node);
|
|
16127
16308
|
if (this.unreachableLines.has(line)) {
|
|
16128
|
-
return;
|
|
16309
|
+
return true;
|
|
16129
16310
|
}
|
|
16130
16311
|
if (this.conditionStack.length > 0 && !this.lineConditions.has(line)) {
|
|
16131
16312
|
this.lineConditions.set(line, this.conditionStack[this.conditionStack.length - 1]);
|
|
@@ -16134,43 +16315,41 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
16134
16315
|
case "method_declaration":
|
|
16135
16316
|
case "constructor_declaration":
|
|
16136
16317
|
this.handleMethodDeclaration(node);
|
|
16137
|
-
return;
|
|
16318
|
+
return true;
|
|
16138
16319
|
// Don't visit children directly, handleMethodDeclaration does it
|
|
16139
16320
|
case "local_variable_declaration":
|
|
16140
16321
|
this.handleVariableDeclaration(node);
|
|
16141
|
-
|
|
16322
|
+
return false;
|
|
16142
16323
|
case "assignment_expression":
|
|
16143
16324
|
this.handleAssignment(node);
|
|
16144
|
-
|
|
16325
|
+
return false;
|
|
16145
16326
|
case "update_expression":
|
|
16146
16327
|
this.handleUpdateExpression(node);
|
|
16147
|
-
|
|
16328
|
+
return false;
|
|
16148
16329
|
case "if_statement":
|
|
16149
16330
|
this.handleIfStatement(node);
|
|
16150
|
-
return;
|
|
16331
|
+
return true;
|
|
16151
16332
|
case "switch_expression":
|
|
16152
16333
|
case "switch_statement":
|
|
16153
16334
|
this.handleSwitch(node);
|
|
16154
|
-
return;
|
|
16335
|
+
return true;
|
|
16155
16336
|
case "ternary_expression":
|
|
16156
16337
|
this.handleTernary(node);
|
|
16157
|
-
|
|
16338
|
+
return false;
|
|
16158
16339
|
case "expression_statement":
|
|
16159
16340
|
this.handleExpressionStatement(node);
|
|
16160
|
-
|
|
16341
|
+
return false;
|
|
16161
16342
|
case "for_statement":
|
|
16162
16343
|
case "enhanced_for_statement":
|
|
16163
16344
|
case "while_statement":
|
|
16164
16345
|
case "do_statement":
|
|
16165
16346
|
this.handleLoopStatement(node);
|
|
16166
|
-
return;
|
|
16347
|
+
return true;
|
|
16167
16348
|
case "synchronized_statement":
|
|
16168
16349
|
this.handleSynchronizedStatement(node);
|
|
16169
|
-
return;
|
|
16350
|
+
return true;
|
|
16170
16351
|
default:
|
|
16171
|
-
|
|
16172
|
-
this.visit(child);
|
|
16173
|
-
}
|
|
16352
|
+
return false;
|
|
16174
16353
|
}
|
|
16175
16354
|
}
|
|
16176
16355
|
/**
|
|
@@ -16938,6 +17117,19 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
16938
17117
|
return null;
|
|
16939
17118
|
}
|
|
16940
17119
|
isTaintedExpression(node) {
|
|
17120
|
+
const stack = [node];
|
|
17121
|
+
while (stack.length > 0) {
|
|
17122
|
+
const current = stack.pop();
|
|
17123
|
+
const result = this.isTaintedExpressionStep(current);
|
|
17124
|
+
if (result === true) return true;
|
|
17125
|
+
if (result === false) continue;
|
|
17126
|
+
for (let i2 = current.children.length - 1; i2 >= 0; i2--) {
|
|
17127
|
+
stack.push(current.children[i2]);
|
|
17128
|
+
}
|
|
17129
|
+
}
|
|
17130
|
+
return false;
|
|
17131
|
+
}
|
|
17132
|
+
isTaintedExpressionStep(node) {
|
|
16941
17133
|
const text = getNodeText2(node, this.source);
|
|
16942
17134
|
if (node.type === "method_invocation") {
|
|
16943
17135
|
const nameNode = node.childForFieldName("name");
|
|
@@ -17197,12 +17389,7 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
17197
17389
|
}
|
|
17198
17390
|
return isTainted;
|
|
17199
17391
|
}
|
|
17200
|
-
|
|
17201
|
-
if (this.isTaintedExpression(child)) {
|
|
17202
|
-
return true;
|
|
17203
|
-
}
|
|
17204
|
-
}
|
|
17205
|
-
return false;
|
|
17392
|
+
return void 0;
|
|
17206
17393
|
}
|
|
17207
17394
|
checkCollectionTaint(node) {
|
|
17208
17395
|
const objectNode = node.childForFieldName("object");
|
|
@@ -17373,7 +17560,7 @@ function isFalsePositive(result, sinkLine, taintedVar) {
|
|
|
17373
17560
|
if (varValue && varValue.type !== "unknown" && !result.tainted.has(taintedVar)) {
|
|
17374
17561
|
return { isFalsePositive: true, reason: `variable_is_constant: ${varValue.value}` };
|
|
17375
17562
|
}
|
|
17376
|
-
if (result.symbols.
|
|
17563
|
+
if (result.symbols.has(taintedVar) && !result.tainted.has(taintedVar)) {
|
|
17377
17564
|
return { isFalsePositive: true, reason: "variable_not_tainted" };
|
|
17378
17565
|
}
|
|
17379
17566
|
return { isFalsePositive: false, reason: null };
|
|
@@ -17526,19 +17713,17 @@ var BaseLanguagePlugin = class {
|
|
|
17526
17713
|
*/
|
|
17527
17714
|
findNodes(root, type) {
|
|
17528
17715
|
const nodes = [];
|
|
17529
|
-
const
|
|
17530
|
-
|
|
17531
|
-
|
|
17532
|
-
|
|
17716
|
+
const stack = [root];
|
|
17717
|
+
while (stack.length > 0) {
|
|
17718
|
+
const node = stack.pop();
|
|
17719
|
+
if (node.type === type) {
|
|
17720
|
+
nodes.push(node);
|
|
17533
17721
|
}
|
|
17534
|
-
|
|
17535
|
-
|
|
17536
|
-
|
|
17537
|
-
} while (cursor.gotoNextSibling());
|
|
17538
|
-
cursor.gotoParent();
|
|
17722
|
+
for (let i2 = node.childCount - 1; i2 >= 0; i2--) {
|
|
17723
|
+
const child = node.child(i2);
|
|
17724
|
+
if (child) stack.push(child);
|
|
17539
17725
|
}
|
|
17540
|
-
}
|
|
17541
|
-
visit();
|
|
17726
|
+
}
|
|
17542
17727
|
return nodes;
|
|
17543
17728
|
}
|
|
17544
17729
|
/**
|
|
@@ -17894,16 +18079,17 @@ var JavaPlugin = class extends BaseLanguagePlugin {
|
|
|
17894
18079
|
}
|
|
17895
18080
|
}
|
|
17896
18081
|
};
|
|
17897
|
-
const
|
|
18082
|
+
const stack = [tree.rootNode];
|
|
18083
|
+
while (stack.length > 0) {
|
|
18084
|
+
const node = stack.pop();
|
|
17898
18085
|
if (node.type === "field_declaration" || node.type === "local_variable_declaration") {
|
|
17899
18086
|
collectDecl(node);
|
|
17900
18087
|
}
|
|
17901
18088
|
for (let i2 = 0; i2 < node.childCount; i2++) {
|
|
17902
18089
|
const child = node.child(i2);
|
|
17903
|
-
if (child)
|
|
18090
|
+
if (child) stack.push(child);
|
|
17904
18091
|
}
|
|
17905
|
-
}
|
|
17906
|
-
walk(tree.rootNode);
|
|
18092
|
+
}
|
|
17907
18093
|
this._typeMapCache.set(tree, map);
|
|
17908
18094
|
return map;
|
|
17909
18095
|
}
|
|
@@ -20700,16 +20886,18 @@ function extractHtmlContent(rootNode) {
|
|
|
20700
20886
|
return { scriptBlocks, eventHandlers };
|
|
20701
20887
|
}
|
|
20702
20888
|
function walkNode(node, scriptBlocks, eventHandlers) {
|
|
20703
|
-
|
|
20704
|
-
|
|
20705
|
-
|
|
20706
|
-
|
|
20707
|
-
|
|
20708
|
-
|
|
20709
|
-
|
|
20710
|
-
|
|
20711
|
-
|
|
20712
|
-
|
|
20889
|
+
const stack = [node];
|
|
20890
|
+
while (stack.length > 0) {
|
|
20891
|
+
const current = stack.pop();
|
|
20892
|
+
if (current.type === "script_element") {
|
|
20893
|
+
extractScriptBlock(current, scriptBlocks);
|
|
20894
|
+
}
|
|
20895
|
+
if (current.type === "element" || current.type === "self_closing_tag") {
|
|
20896
|
+
extractEventHandlers(current, eventHandlers);
|
|
20897
|
+
}
|
|
20898
|
+
for (let i2 = current.childCount - 1; i2 >= 0; i2--) {
|
|
20899
|
+
const child = current.child(i2);
|
|
20900
|
+
if (child) stack.push(child);
|
|
20713
20901
|
}
|
|
20714
20902
|
}
|
|
20715
20903
|
}
|
|
@@ -20806,13 +20994,15 @@ function runHtmlAttributeSecurityChecks(rootNode, filePath) {
|
|
|
20806
20994
|
return findings;
|
|
20807
20995
|
}
|
|
20808
20996
|
function walkForSecurityChecks(node, filePath, findings) {
|
|
20809
|
-
|
|
20810
|
-
|
|
20811
|
-
|
|
20812
|
-
|
|
20813
|
-
|
|
20814
|
-
|
|
20815
|
-
|
|
20997
|
+
const stack = [node];
|
|
20998
|
+
while (stack.length > 0) {
|
|
20999
|
+
const current = stack.pop();
|
|
21000
|
+
if (current.type === "element" || current.type === "self_closing_tag" || current.type === "script_element" || current.type === "style_element") {
|
|
21001
|
+
checkElement(current, filePath, findings);
|
|
21002
|
+
}
|
|
21003
|
+
for (let i2 = current.childCount - 1; i2 >= 0; i2--) {
|
|
21004
|
+
const child = current.child(i2);
|
|
21005
|
+
if (child) stack.push(child);
|
|
20816
21006
|
}
|
|
20817
21007
|
}
|
|
20818
21008
|
}
|
|
@@ -21688,6 +21878,37 @@ function buildJavaScriptTaintedVars(sourceCode, language) {
|
|
|
21688
21878
|
}
|
|
21689
21879
|
return tainted;
|
|
21690
21880
|
}
|
|
21881
|
+
function buildRustTaintedVars(sourceCode, seedVars) {
|
|
21882
|
+
const derived = /* @__PURE__ */ new Map();
|
|
21883
|
+
const knownTainted = new Set(seedVars);
|
|
21884
|
+
const lines = sourceCode.split("\n");
|
|
21885
|
+
let changed = true;
|
|
21886
|
+
while (changed) {
|
|
21887
|
+
changed = false;
|
|
21888
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
21889
|
+
const line = lines[i2];
|
|
21890
|
+
const trimmed = line.trimStart();
|
|
21891
|
+
if (trimmed.startsWith("//")) continue;
|
|
21892
|
+
const letMatch = line.match(
|
|
21893
|
+
/^\s*let\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::\s*[^=]+)?=\s*(.+?)(?:;|$)/
|
|
21894
|
+
);
|
|
21895
|
+
const assignMatch = !letMatch ? line.match(/^\s*([A-Za-z_]\w*)\s*=\s*(.+?)(?:;|$)/) : null;
|
|
21896
|
+
const m = letMatch ?? assignMatch;
|
|
21897
|
+
if (!m) continue;
|
|
21898
|
+
const lhs = m[1];
|
|
21899
|
+
const rhs = m[2];
|
|
21900
|
+
if (lhs === "if" || lhs === "while" || lhs === "for" || lhs === "match" || lhs === "return") continue;
|
|
21901
|
+
if (knownTainted.has(lhs)) continue;
|
|
21902
|
+
const ref = [...knownTainted].some((v) => new RegExp(`\\b${v}\\b`).test(rhs));
|
|
21903
|
+
if (ref) {
|
|
21904
|
+
derived.set(lhs, i2 + 1);
|
|
21905
|
+
knownTainted.add(lhs);
|
|
21906
|
+
changed = true;
|
|
21907
|
+
}
|
|
21908
|
+
}
|
|
21909
|
+
}
|
|
21910
|
+
return derived;
|
|
21911
|
+
}
|
|
21691
21912
|
var BASH_UNTRUSTED_ENV_PATTERNS = [
|
|
21692
21913
|
/^USER_INPUT$/i,
|
|
21693
21914
|
/^QUERY_STRING$/i,
|
|
@@ -22087,7 +22308,20 @@ function evaluateSimpleExpression(expr, symbols) {
|
|
|
22087
22308
|
}
|
|
22088
22309
|
function isStringLiteralExpression(expr) {
|
|
22089
22310
|
const trimmed = expr.trim();
|
|
22090
|
-
|
|
22311
|
+
if (trimmed.length < 2) return false;
|
|
22312
|
+
const quote = trimmed[0];
|
|
22313
|
+
if (quote !== '"' && quote !== "'") return false;
|
|
22314
|
+
let i2 = 1;
|
|
22315
|
+
while (i2 < trimmed.length) {
|
|
22316
|
+
const c = trimmed[i2];
|
|
22317
|
+
if (c === "\\") {
|
|
22318
|
+
i2 += 2;
|
|
22319
|
+
continue;
|
|
22320
|
+
}
|
|
22321
|
+
if (c === quote) return i2 === trimmed.length - 1;
|
|
22322
|
+
i2++;
|
|
22323
|
+
}
|
|
22324
|
+
return false;
|
|
22091
22325
|
}
|
|
22092
22326
|
function filterCleanArraySinks(sinks, calls, taintedArrayElements, symbols) {
|
|
22093
22327
|
const callsByLine = /* @__PURE__ */ new Map();
|
|
@@ -22272,7 +22506,7 @@ var TaintPropagationPass = class {
|
|
|
22272
22506
|
flows.push(f);
|
|
22273
22507
|
}
|
|
22274
22508
|
}
|
|
22275
|
-
const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, constProp.unreachableLines, ctx.code, ctx.language) ?? [];
|
|
22509
|
+
const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, ctx.code, ctx.language) ?? [];
|
|
22276
22510
|
for (const f of exprScanFlows) {
|
|
22277
22511
|
if (flows.some(
|
|
22278
22512
|
(x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type
|
|
@@ -22484,14 +22718,63 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
22484
22718
|
void types;
|
|
22485
22719
|
return flows;
|
|
22486
22720
|
}
|
|
22487
|
-
function detectExpressionScanFlows(calls, sources, sinks, unreachableLines, code, language) {
|
|
22721
|
+
function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, code, language) {
|
|
22488
22722
|
const flows = [];
|
|
22489
22723
|
const sourcesWithVar = sources.filter(
|
|
22490
22724
|
(s) => typeof s.variable === "string" && s.variable.length > 0
|
|
22491
22725
|
);
|
|
22492
22726
|
if (sourcesWithVar.length === 0) return flows;
|
|
22727
|
+
const aliasSanitizedFor = /* @__PURE__ */ new Map();
|
|
22493
22728
|
if (language === "python" && typeof code === "string") {
|
|
22494
22729
|
const derived = buildPythonTaintedVars(code);
|
|
22730
|
+
if (derived.size > 0) {
|
|
22731
|
+
let anchor = sourcesWithVar[0];
|
|
22732
|
+
for (const s of sourcesWithVar) {
|
|
22733
|
+
if (s.line < anchor.line) anchor = s;
|
|
22734
|
+
}
|
|
22735
|
+
const existingVars = new Set(sourcesWithVar.map((s) => s.variable));
|
|
22736
|
+
for (const [varName] of derived) {
|
|
22737
|
+
if (!varName || existingVars.has(varName)) continue;
|
|
22738
|
+
sourcesWithVar.push({
|
|
22739
|
+
...anchor,
|
|
22740
|
+
variable: varName
|
|
22741
|
+
});
|
|
22742
|
+
existingVars.add(varName);
|
|
22743
|
+
}
|
|
22744
|
+
if (sanitizers && sanitizers.length > 0) {
|
|
22745
|
+
const sanitizersByLine = /* @__PURE__ */ new Map();
|
|
22746
|
+
for (const s of sanitizers) {
|
|
22747
|
+
const arr = sanitizersByLine.get(s.line) ?? [];
|
|
22748
|
+
arr.push(s);
|
|
22749
|
+
sanitizersByLine.set(s.line, arr);
|
|
22750
|
+
}
|
|
22751
|
+
const codeLines = code.split("\n");
|
|
22752
|
+
for (const [varName, originLine] of derived) {
|
|
22753
|
+
const lineSans = sanitizersByLine.get(originLine);
|
|
22754
|
+
if (!lineSans || lineSans.length === 0) continue;
|
|
22755
|
+
const lineText = codeLines[originLine - 1] ?? "";
|
|
22756
|
+
const rhsMatch = lineText.match(/^\s*\w+\s*=\s*(.+)$/);
|
|
22757
|
+
if (!rhsMatch) continue;
|
|
22758
|
+
const rhs = rhsMatch[1];
|
|
22759
|
+
for (const san of lineSans) {
|
|
22760
|
+
const sanMatch = san.method.match(/^(?:(\w+)\.)?(\w+)\(\)$/);
|
|
22761
|
+
if (!sanMatch) continue;
|
|
22762
|
+
const sanName = sanMatch[1] ? `${sanMatch[1]}.${sanMatch[2]}` : sanMatch[2];
|
|
22763
|
+
if (!rhs.includes(`${sanName}(`)) continue;
|
|
22764
|
+
let set = aliasSanitizedFor.get(varName);
|
|
22765
|
+
if (!set) {
|
|
22766
|
+
set = /* @__PURE__ */ new Set();
|
|
22767
|
+
aliasSanitizedFor.set(varName, set);
|
|
22768
|
+
}
|
|
22769
|
+
for (const t of san.sanitizes) set.add(t);
|
|
22770
|
+
}
|
|
22771
|
+
}
|
|
22772
|
+
}
|
|
22773
|
+
}
|
|
22774
|
+
}
|
|
22775
|
+
if (language === "rust" && typeof code === "string") {
|
|
22776
|
+
const seedVars = new Set(sourcesWithVar.map((s) => s.variable));
|
|
22777
|
+
const derived = buildRustTaintedVars(code, seedVars);
|
|
22495
22778
|
if (derived.size > 0) {
|
|
22496
22779
|
let anchor = sourcesWithVar[0];
|
|
22497
22780
|
for (const s of sourcesWithVar) {
|
|
@@ -22537,6 +22820,9 @@ function detectExpressionScanFlows(calls, sources, sinks, unreachableLines, code
|
|
|
22537
22820
|
if (flows.some(
|
|
22538
22821
|
(f) => f.source_line === source.line && f.sink_line === sink.line && f.sink_type === sink.type
|
|
22539
22822
|
)) continue;
|
|
22823
|
+
if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
|
|
22824
|
+
break;
|
|
22825
|
+
}
|
|
22540
22826
|
flows.push({
|
|
22541
22827
|
source_line: source.line,
|
|
22542
22828
|
sink_line: sink.line,
|
|
@@ -22681,6 +22967,9 @@ var InterproceduralPass = class {
|
|
|
22681
22967
|
}
|
|
22682
22968
|
}
|
|
22683
22969
|
}
|
|
22970
|
+
if (additionalSinks.length > 0) {
|
|
22971
|
+
attachSourceLineCode([], additionalSinks, ctx.code);
|
|
22972
|
+
}
|
|
22684
22973
|
return { additionalSinks, additionalFlows, interprocedural };
|
|
22685
22974
|
}
|
|
22686
22975
|
};
|
|
@@ -26679,6 +26968,70 @@ function isPotentialPojo(type) {
|
|
|
26679
26968
|
return first >= 65 && first <= 90;
|
|
26680
26969
|
}
|
|
26681
26970
|
|
|
26971
|
+
// src/analysis/passes/insecure-cookie-pass.ts
|
|
26972
|
+
var COOKIE_RESPONSE_RECEIVERS = /* @__PURE__ */ new Set([
|
|
26973
|
+
"res",
|
|
26974
|
+
"response",
|
|
26975
|
+
"reply"
|
|
26976
|
+
]);
|
|
26977
|
+
var SECURE_TRUE_RE = /\bsecure\s*:\s*true\b/;
|
|
26978
|
+
var HTTPONLY_TRUE_RE = /\bhttpOnly\s*:\s*true\b/i;
|
|
26979
|
+
var InsecureCookiePass = class {
|
|
26980
|
+
name = "insecure-cookie";
|
|
26981
|
+
category = "security";
|
|
26982
|
+
run(ctx) {
|
|
26983
|
+
const { graph, language } = ctx;
|
|
26984
|
+
if (language !== "javascript" && language !== "typescript") {
|
|
26985
|
+
return { insecureCookies: [] };
|
|
26986
|
+
}
|
|
26987
|
+
const file = graph.ir.meta.file;
|
|
26988
|
+
const insecureCookies = [];
|
|
26989
|
+
for (const call of graph.ir.calls) {
|
|
26990
|
+
if (call.method_name !== "cookie") continue;
|
|
26991
|
+
const receiver = call.receiver ?? "";
|
|
26992
|
+
if (!COOKIE_RESPONSE_RECEIVERS.has(receiver)) continue;
|
|
26993
|
+
if (call.arguments.length < 2) continue;
|
|
26994
|
+
const opts = call.arguments.find((a) => a.position === 2);
|
|
26995
|
+
const optsExpr = (opts?.expression ?? "").trim();
|
|
26996
|
+
const optionsPresent = optsExpr.length > 0;
|
|
26997
|
+
const missingSecure = !SECURE_TRUE_RE.test(optsExpr);
|
|
26998
|
+
const missingHttpOnly = !HTTPONLY_TRUE_RE.test(optsExpr);
|
|
26999
|
+
if (!missingSecure && !missingHttpOnly) continue;
|
|
27000
|
+
const line = call.location.line;
|
|
27001
|
+
insecureCookies.push({
|
|
27002
|
+
line,
|
|
27003
|
+
receiver,
|
|
27004
|
+
missingSecure,
|
|
27005
|
+
missingHttpOnly,
|
|
27006
|
+
optionsPresent
|
|
27007
|
+
});
|
|
27008
|
+
const missing = [];
|
|
27009
|
+
if (missingSecure) missing.push("`secure: true`");
|
|
27010
|
+
if (missingHttpOnly) missing.push("`httpOnly: true`");
|
|
27011
|
+
ctx.addFinding({
|
|
27012
|
+
id: `${this.name}-${file}-${line}`,
|
|
27013
|
+
pass: this.name,
|
|
27014
|
+
category: this.category,
|
|
27015
|
+
rule_id: this.name,
|
|
27016
|
+
cwe: "CWE-614",
|
|
27017
|
+
severity: "medium",
|
|
27018
|
+
level: "warning",
|
|
27019
|
+
message: `Cookie set without ${missing.join(" and ")} \u2014 vulnerable to cleartext transmission (CWE-614) and client-side JS access (CWE-1004).`,
|
|
27020
|
+
file,
|
|
27021
|
+
line,
|
|
27022
|
+
fix: 'Pass `{ secure: true, httpOnly: true, sameSite: "lax" }` as the third argument to `res.cookie()`.',
|
|
27023
|
+
evidence: {
|
|
27024
|
+
receiver,
|
|
27025
|
+
options_present: optionsPresent,
|
|
27026
|
+
missing_secure: missingSecure,
|
|
27027
|
+
missing_http_only: missingHttpOnly
|
|
27028
|
+
}
|
|
27029
|
+
});
|
|
27030
|
+
}
|
|
27031
|
+
return { insecureCookies };
|
|
27032
|
+
}
|
|
27033
|
+
};
|
|
27034
|
+
|
|
26682
27035
|
// src/analysis/metrics/passes/size-metrics-pass.ts
|
|
26683
27036
|
var SizeMetricsPass = class {
|
|
26684
27037
|
name = "size-metrics";
|
|
@@ -27575,6 +27928,7 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
27575
27928
|
if (!disabledPasses.has("naming-convention")) pipeline.add(new NamingConventionPass(passOpts.namingConvention));
|
|
27576
27929
|
if (!disabledPasses.has("security-headers")) pipeline.add(new SecurityHeadersPass(passOpts.securityHeaders));
|
|
27577
27930
|
if (!disabledPasses.has("spring4shell")) pipeline.add(new Spring4ShellPass());
|
|
27931
|
+
if (!disabledPasses.has("insecure-cookie")) pipeline.add(new InsecureCookiePass());
|
|
27578
27932
|
const { results, findings } = pipeline.run(graph, code, language, config);
|
|
27579
27933
|
const sinkFilter = results.get("sink-filter");
|
|
27580
27934
|
const interProc = results.get("interprocedural");
|