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
|
@@ -4752,14 +4752,46 @@ function extractJSClassInfo(node) {
|
|
|
4752
4752
|
end_line: node.endPosition.row + 1
|
|
4753
4753
|
};
|
|
4754
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
|
+
}
|
|
4755
4775
|
function extractJSMethods(body2) {
|
|
4756
4776
|
const methods = [];
|
|
4777
|
+
let pendingDecorators = [];
|
|
4757
4778
|
for (let i2 = 0; i2 < body2.childCount; i2++) {
|
|
4758
4779
|
const child = body2.child(i2);
|
|
4759
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;
|
|
4760
4787
|
if (child.type === "method_definition") {
|
|
4761
|
-
|
|
4788
|
+
const m = extractJSMethodInfo(child);
|
|
4789
|
+
if (pendingDecorators.length > 0) {
|
|
4790
|
+
m.annotations = pendingDecorators;
|
|
4791
|
+
}
|
|
4792
|
+
methods.push(m);
|
|
4762
4793
|
}
|
|
4794
|
+
pendingDecorators = [];
|
|
4763
4795
|
}
|
|
4764
4796
|
return methods;
|
|
4765
4797
|
}
|
|
@@ -4921,10 +4953,18 @@ function extractJSParameters(params) {
|
|
|
4921
4953
|
if (typeNode) {
|
|
4922
4954
|
paramType = getNodeText(typeNode).replace(/^:\s*/, "");
|
|
4923
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
|
+
}
|
|
4924
4964
|
parameters.push({
|
|
4925
4965
|
name: paramName,
|
|
4926
4966
|
type: paramType,
|
|
4927
|
-
annotations:
|
|
4967
|
+
annotations: decorators,
|
|
4928
4968
|
line: child.startPosition.row + 1
|
|
4929
4969
|
});
|
|
4930
4970
|
}
|
|
@@ -11635,6 +11675,26 @@ var DEFAULT_SINKS = [
|
|
|
11635
11675
|
{ method: "setQuotedArgumentsEnabled", class: "Shell", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0] },
|
|
11636
11676
|
// Sandbox/script security
|
|
11637
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"] },
|
|
11638
11698
|
// =========================================================================
|
|
11639
11699
|
// Node.js/Express Sinks
|
|
11640
11700
|
// =========================================================================
|
|
@@ -11687,13 +11747,47 @@ var DEFAULT_SINKS = [
|
|
|
11687
11747
|
{ method: "runInContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
11688
11748
|
{ method: "runInNewContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
11689
11749
|
{ method: "runInThisContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
11690
|
-
// 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.
|
|
11691
11758
|
{ method: "find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11692
11759
|
{ method: "findOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11693
11760
|
{ method: "updateOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11694
11761
|
{ method: "updateMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11695
11762
|
{ method: "deleteOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11696
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"] },
|
|
11697
11791
|
// Node.js SSRF (HTTP clients)
|
|
11698
11792
|
{ method: "get", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
11699
11793
|
{ method: "post", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
@@ -11715,6 +11809,24 @@ var DEFAULT_SINKS = [
|
|
|
11715
11809
|
{ method: "post", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
11716
11810
|
// node-fetch
|
|
11717
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"] },
|
|
11718
11830
|
// =========================================================================
|
|
11719
11831
|
// Python Sinks
|
|
11720
11832
|
// =========================================================================
|
|
@@ -11756,7 +11868,12 @@ var DEFAULT_SINKS = [
|
|
|
11756
11868
|
{ method: "rmtree", class: "shutil", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0] },
|
|
11757
11869
|
{ method: "send_file", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
|
|
11758
11870
|
// Python XSS / SSTI
|
|
11759
|
-
|
|
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"] },
|
|
11760
11877
|
{ method: "Markup", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
|
|
11761
11878
|
{ method: "mark_safe", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
|
|
11762
11879
|
// Python SSRF
|
|
@@ -12119,6 +12236,13 @@ var DEFAULT_SANITIZERS = [
|
|
|
12119
12236
|
{ method: "secure_filename", class: "werkzeug.utils", removes: ["path_traversal"] },
|
|
12120
12237
|
{ method: "basename", class: "os.path", removes: ["path_traversal"] },
|
|
12121
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"] },
|
|
12122
12246
|
// Python Type coercion
|
|
12123
12247
|
{ method: "int", removes: ["sql_injection", "command_injection", "xss"] },
|
|
12124
12248
|
{ method: "float", removes: ["sql_injection", "command_injection"] },
|
|
@@ -12267,7 +12391,7 @@ var PYTHON_TAINTED_PATTERNS = [
|
|
|
12267
12391
|
];
|
|
12268
12392
|
function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
|
|
12269
12393
|
const sourceLines = code !== void 0 ? code.split("\n") : void 0;
|
|
12270
|
-
const sources = findSources(calls, types, config.sources, sourceLines);
|
|
12394
|
+
const sources = findSources(calls, types, config.sources, sourceLines, language);
|
|
12271
12395
|
const sinks = findSinks(calls, config.sinks, typeHierarchy, language, sourceLines);
|
|
12272
12396
|
const sanitizers = findSanitizers(calls, types, config.sanitizers);
|
|
12273
12397
|
return { sources, sinks, sanitizers };
|
|
@@ -12285,7 +12409,7 @@ function attachSourceLineCode(sources, sinks, code) {
|
|
|
12285
12409
|
}
|
|
12286
12410
|
}
|
|
12287
12411
|
}
|
|
12288
|
-
function findSources(calls, types, patterns, sourceLines) {
|
|
12412
|
+
function findSources(calls, types, patterns, sourceLines, language) {
|
|
12289
12413
|
const sources = [];
|
|
12290
12414
|
for (const call of calls) {
|
|
12291
12415
|
for (const pattern of patterns) {
|
|
@@ -12338,23 +12462,29 @@ function findSources(calls, types, patterns, sourceLines) {
|
|
|
12338
12462
|
}
|
|
12339
12463
|
}
|
|
12340
12464
|
}
|
|
12341
|
-
const
|
|
12465
|
+
const RUST_EXTRACTOR_KIND = /(?:^|::)(Json|Form|Query|Path|Extension|Multipart|Body|Bytes)(?:<|$)/;
|
|
12342
12466
|
for (const type of types) {
|
|
12343
12467
|
for (const method of type.methods) {
|
|
12344
12468
|
for (const param of method.parameters) {
|
|
12345
|
-
if (param.type
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
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
|
+
});
|
|
12358
12488
|
}
|
|
12359
12489
|
}
|
|
12360
12490
|
}
|
|
@@ -12436,6 +12566,15 @@ function findSources(calls, types, patterns, sourceLines) {
|
|
|
12436
12566
|
s.code = sourceLines[s.line - 1]?.trim();
|
|
12437
12567
|
}
|
|
12438
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
|
+
}
|
|
12439
12578
|
return result;
|
|
12440
12579
|
}
|
|
12441
12580
|
function isInterproceduralTaintableType(typeName) {
|
|
@@ -12541,6 +12680,20 @@ function isParameterizedQueryCall(call, pattern) {
|
|
|
12541
12680
|
}
|
|
12542
12681
|
return false;
|
|
12543
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
|
+
}
|
|
12544
12697
|
var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
|
|
12545
12698
|
function argIsClassLiteral(call, position) {
|
|
12546
12699
|
const arg = call.arguments.find((a) => a.position === position);
|
|
@@ -12557,6 +12710,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
|
|
|
12557
12710
|
if (isParameterizedQueryCall(call, pattern)) {
|
|
12558
12711
|
continue;
|
|
12559
12712
|
}
|
|
12713
|
+
if (isSafePythonSubprocessCall(call, pattern, language)) {
|
|
12714
|
+
continue;
|
|
12715
|
+
}
|
|
12560
12716
|
if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
|
|
12561
12717
|
continue;
|
|
12562
12718
|
}
|
|
@@ -12959,7 +13115,12 @@ function receiverMightBeClass(receiver, className) {
|
|
|
12959
13115
|
"controller",
|
|
12960
13116
|
"task",
|
|
12961
13117
|
"thread",
|
|
12962
|
-
"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"
|
|
12963
13124
|
]);
|
|
12964
13125
|
const isAmbiguous = ambiguousIdentifiers.has(lowerReceiver);
|
|
12965
13126
|
if (!isAmbiguous && lowerReceiver.length >= 3 && lowerClass.includes(lowerReceiver)) {
|
|
@@ -12969,7 +13130,9 @@ function receiverMightBeClass(receiver, className) {
|
|
|
12969
13130
|
}
|
|
12970
13131
|
if (!isAmbiguous && lowerReceiver.length >= 2) {
|
|
12971
13132
|
if (lowerClass.startsWith(lowerReceiver) || lowerClass.endsWith(lowerReceiver)) {
|
|
12972
|
-
|
|
13133
|
+
if (lowerReceiver.length / lowerClass.length >= 0.4) {
|
|
13134
|
+
return true;
|
|
13135
|
+
}
|
|
12973
13136
|
}
|
|
12974
13137
|
}
|
|
12975
13138
|
if (!isAmbiguous && lowerReceiver.length >= 3) {
|
|
@@ -12992,6 +13155,9 @@ function receiverMightBeClass(receiver, className) {
|
|
|
12992
13155
|
ps: ["PreparedStatement"],
|
|
12993
13156
|
rs: ["ResultSet"],
|
|
12994
13157
|
template: ["JdbcTemplate"],
|
|
13158
|
+
cur: ["Cursor"],
|
|
13159
|
+
// Python DB-API cursor — see ambiguousIdentifiers note
|
|
13160
|
+
cursor: ["Cursor"],
|
|
12995
13161
|
// I/O
|
|
12996
13162
|
writer: ["PrintWriter"],
|
|
12997
13163
|
out: ["PrintWriter", "OutputStream"],
|
|
@@ -17394,7 +17560,7 @@ function isFalsePositive(result, sinkLine, taintedVar) {
|
|
|
17394
17560
|
if (varValue && varValue.type !== "unknown" && !result.tainted.has(taintedVar)) {
|
|
17395
17561
|
return { isFalsePositive: true, reason: `variable_is_constant: ${varValue.value}` };
|
|
17396
17562
|
}
|
|
17397
|
-
if (result.symbols.
|
|
17563
|
+
if (result.symbols.has(taintedVar) && !result.tainted.has(taintedVar)) {
|
|
17398
17564
|
return { isFalsePositive: true, reason: "variable_not_tainted" };
|
|
17399
17565
|
}
|
|
17400
17566
|
return { isFalsePositive: false, reason: null };
|
|
@@ -21712,6 +21878,37 @@ function buildJavaScriptTaintedVars(sourceCode, language) {
|
|
|
21712
21878
|
}
|
|
21713
21879
|
return tainted;
|
|
21714
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
|
+
}
|
|
21715
21912
|
var BASH_UNTRUSTED_ENV_PATTERNS = [
|
|
21716
21913
|
/^USER_INPUT$/i,
|
|
21717
21914
|
/^QUERY_STRING$/i,
|
|
@@ -22111,7 +22308,20 @@ function evaluateSimpleExpression(expr, symbols) {
|
|
|
22111
22308
|
}
|
|
22112
22309
|
function isStringLiteralExpression(expr) {
|
|
22113
22310
|
const trimmed = expr.trim();
|
|
22114
|
-
|
|
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;
|
|
22115
22325
|
}
|
|
22116
22326
|
function filterCleanArraySinks(sinks, calls, taintedArrayElements, symbols) {
|
|
22117
22327
|
const callsByLine = /* @__PURE__ */ new Map();
|
|
@@ -22296,7 +22506,7 @@ var TaintPropagationPass = class {
|
|
|
22296
22506
|
flows.push(f);
|
|
22297
22507
|
}
|
|
22298
22508
|
}
|
|
22299
|
-
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) ?? [];
|
|
22300
22510
|
for (const f of exprScanFlows) {
|
|
22301
22511
|
if (flows.some(
|
|
22302
22512
|
(x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type
|
|
@@ -22508,14 +22718,63 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
22508
22718
|
void types;
|
|
22509
22719
|
return flows;
|
|
22510
22720
|
}
|
|
22511
|
-
function detectExpressionScanFlows(calls, sources, sinks, unreachableLines, code, language) {
|
|
22721
|
+
function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, code, language) {
|
|
22512
22722
|
const flows = [];
|
|
22513
22723
|
const sourcesWithVar = sources.filter(
|
|
22514
22724
|
(s) => typeof s.variable === "string" && s.variable.length > 0
|
|
22515
22725
|
);
|
|
22516
22726
|
if (sourcesWithVar.length === 0) return flows;
|
|
22727
|
+
const aliasSanitizedFor = /* @__PURE__ */ new Map();
|
|
22517
22728
|
if (language === "python" && typeof code === "string") {
|
|
22518
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);
|
|
22519
22778
|
if (derived.size > 0) {
|
|
22520
22779
|
let anchor = sourcesWithVar[0];
|
|
22521
22780
|
for (const s of sourcesWithVar) {
|
|
@@ -22561,6 +22820,9 @@ function detectExpressionScanFlows(calls, sources, sinks, unreachableLines, code
|
|
|
22561
22820
|
if (flows.some(
|
|
22562
22821
|
(f) => f.source_line === source.line && f.sink_line === sink.line && f.sink_type === sink.type
|
|
22563
22822
|
)) continue;
|
|
22823
|
+
if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
|
|
22824
|
+
break;
|
|
22825
|
+
}
|
|
22564
22826
|
flows.push({
|
|
22565
22827
|
source_line: source.line,
|
|
22566
22828
|
sink_line: sink.line,
|
|
@@ -22705,6 +22967,9 @@ var InterproceduralPass = class {
|
|
|
22705
22967
|
}
|
|
22706
22968
|
}
|
|
22707
22969
|
}
|
|
22970
|
+
if (additionalSinks.length > 0) {
|
|
22971
|
+
attachSourceLineCode([], additionalSinks, ctx.code);
|
|
22972
|
+
}
|
|
22708
22973
|
return { additionalSinks, additionalFlows, interprocedural };
|
|
22709
22974
|
}
|
|
22710
22975
|
};
|
|
@@ -26703,6 +26968,70 @@ function isPotentialPojo(type) {
|
|
|
26703
26968
|
return first >= 65 && first <= 90;
|
|
26704
26969
|
}
|
|
26705
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
|
+
|
|
26706
27035
|
// src/analysis/metrics/passes/size-metrics-pass.ts
|
|
26707
27036
|
var SizeMetricsPass = class {
|
|
26708
27037
|
name = "size-metrics";
|
|
@@ -27599,6 +27928,7 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
27599
27928
|
if (!disabledPasses.has("naming-convention")) pipeline.add(new NamingConventionPass(passOpts.namingConvention));
|
|
27600
27929
|
if (!disabledPasses.has("security-headers")) pipeline.add(new SecurityHeadersPass(passOpts.securityHeaders));
|
|
27601
27930
|
if (!disabledPasses.has("spring4shell")) pipeline.add(new Spring4ShellPass());
|
|
27931
|
+
if (!disabledPasses.has("insecure-cookie")) pipeline.add(new InsecureCookiePass());
|
|
27602
27932
|
const { results, findings } = pipeline.run(graph, code, language, config);
|
|
27603
27933
|
const sinkFilter = results.get("sink-filter");
|
|
27604
27934
|
const interProc = results.get("interprocedural");
|