circle-ir 3.178.0 → 3.180.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 +30 -0
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/dependency-versions.d.ts +171 -0
- package/dist/analysis/dependency-versions.d.ts.map +1 -1
- package/dist/analysis/dependency-versions.js +348 -0
- package/dist/analysis/dependency-versions.js.map +1 -1
- package/dist/analysis/passes/deserialization-safety-gate-pass.d.ts +2 -0
- package/dist/analysis/passes/deserialization-safety-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/deserialization-safety-gate-pass.js +56 -9
- package/dist/analysis/passes/deserialization-safety-gate-pass.js.map +1 -1
- package/dist/analysis/passes/python-receiver-taint-format-pass.d.ts +48 -0
- package/dist/analysis/passes/python-receiver-taint-format-pass.d.ts.map +1 -0
- package/dist/analysis/passes/python-receiver-taint-format-pass.js +91 -0
- package/dist/analysis/passes/python-receiver-taint-format-pass.js.map +1 -0
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +18 -0
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts +46 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +9 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +229 -5
- package/dist/core/circle-ir-core.cjs +35 -0
- package/dist/core/circle-ir-core.js +35 -0
- package/package.json +1 -1
|
@@ -38,13 +38,16 @@
|
|
|
38
38
|
*
|
|
39
39
|
* See `docs/PASSES.md` for the canonical pass registry entry.
|
|
40
40
|
*/
|
|
41
|
-
import { resolveFastjsonFromPom, fileReenablesFastjsonAutotype, fileEnablesJacksonPolymorphism, fileConfiguresSnakeYamlSafely, } from '../dependency-versions.js';
|
|
41
|
+
import { resolveFastjsonFromPom, resolveFastjsonFromGradle, resolveFastjsonFromGradleCatalog, resolvePyYamlFromRequirements, resolvePyYamlFromPyproject, fileHasUnsafePyYamlLoader, fileReenablesFastjsonAutotype, fileEnablesJacksonPolymorphism, fileConfiguresSnakeYamlSafely, } from '../dependency-versions.js';
|
|
42
42
|
const FASTJSON_METHODS = new Set(['parseObject', 'parse']);
|
|
43
43
|
const FASTJSON_CLASSES = new Set(['JSON', 'JSONObject']);
|
|
44
44
|
const JACKSON_METHODS = new Set(['readValue', 'convertValue', 'treeToValue']);
|
|
45
45
|
const JACKSON_CLASSES = new Set(['ObjectMapper', 'ObjectReader']);
|
|
46
46
|
const SNAKEYAML_METHODS = new Set(['load', 'loadAs', 'loadAll']);
|
|
47
47
|
const SNAKEYAML_CLASSES = new Set(['Yaml']);
|
|
48
|
+
// Python PyYAML gate (Gate D — cognium-dev #261 Python slice).
|
|
49
|
+
const PYYAML_METHODS = new Set(['load']);
|
|
50
|
+
const PYYAML_CLASSES = new Set(['yaml']);
|
|
48
51
|
export class DeserializationSafetyGatePass {
|
|
49
52
|
dependencyContext;
|
|
50
53
|
name = 'deserialization-safety-gate';
|
|
@@ -54,11 +57,11 @@ export class DeserializationSafetyGatePass {
|
|
|
54
57
|
}
|
|
55
58
|
run(ctx) {
|
|
56
59
|
const { graph, language, code } = ctx;
|
|
57
|
-
// Java
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
if (language !== 'java') {
|
|
61
|
-
return { droppedFastjson: 0, droppedJackson: 0, droppedSnakeYaml: 0 };
|
|
60
|
+
// Java + Python for the MVP. Other languages (JS Deserialize,
|
|
61
|
+
// Rust bincode) can extend the gate on their own manifests in
|
|
62
|
+
// follow-up scopes.
|
|
63
|
+
if (language !== 'java' && language !== 'python') {
|
|
64
|
+
return { droppedFastjson: 0, droppedJackson: 0, droppedSnakeYaml: 0, droppedPyYaml: 0 };
|
|
62
65
|
}
|
|
63
66
|
// Same sink-source discovery as SinkSemanticsPass: prefer
|
|
64
67
|
// SinkFilterResult when the real pipeline has run, otherwise fall
|
|
@@ -68,17 +71,48 @@ export class DeserializationSafetyGatePass {
|
|
|
68
71
|
? ctx.getResult('sink-filter').sinks
|
|
69
72
|
: graph.ir.taint.sinks;
|
|
70
73
|
// --- Gate A: Fastjson _noneautotype ------------------------------------
|
|
74
|
+
// Consult pom.xml first; fall back to build.gradle when pom does
|
|
75
|
+
// not resolve. Both manifests may be supplied by a caller that
|
|
76
|
+
// scans a multi-build project; either one indicating the hardened
|
|
77
|
+
// classifier is sufficient to drop the sink.
|
|
71
78
|
const pomXml = this.dependencyContext?.java?.pomXml;
|
|
72
|
-
const
|
|
79
|
+
const buildGradle = this.dependencyContext?.java?.buildGradle;
|
|
80
|
+
const libsVersionsToml = this.dependencyContext?.java?.libsVersionsToml;
|
|
81
|
+
const fastjson = (pomXml ? resolveFastjsonFromPom(pomXml) : null) ??
|
|
82
|
+
(buildGradle ? resolveFastjsonFromGradle(buildGradle) : null) ??
|
|
83
|
+
(buildGradle && libsVersionsToml
|
|
84
|
+
? resolveFastjsonFromGradleCatalog(buildGradle, libsVersionsToml)
|
|
85
|
+
: null);
|
|
73
86
|
const fastjsonHardened = fastjson?.noneAutotype === true &&
|
|
74
87
|
!fileReenablesFastjsonAutotype(code);
|
|
75
88
|
// --- Gate B: Jackson polymorphism ---------------------------------------
|
|
76
89
|
const jacksonSafe = !fileEnablesJacksonPolymorphism(code);
|
|
77
90
|
// --- Gate C: SnakeYAML SafeConstructor ----------------------------------
|
|
78
91
|
const snakeYamlSafe = fileConfiguresSnakeYamlSafely(code);
|
|
92
|
+
// --- Gate D: PyYAML ≥ 6.0 (Python) --------------------------------------
|
|
93
|
+
// Under pyyaml ≥ 6.0, `yaml.load(x)` without an explicit `Loader=`
|
|
94
|
+
// keyword arg raises TypeError (safe-by-default); callers that pass
|
|
95
|
+
// `Loader=SafeLoader` are safe; callers that pass an unsafe Loader
|
|
96
|
+
// (`Loader=Loader` / `UnsafeLoader` / `FullLoader`) are still
|
|
97
|
+
// dangerous regardless of the version pin — those need per-call
|
|
98
|
+
// inspection to preserve.
|
|
99
|
+
const requirementsTxt = this.dependencyContext?.python?.requirementsTxt;
|
|
100
|
+
const pyprojectToml = this.dependencyContext?.python?.pyprojectToml;
|
|
101
|
+
const pyYaml = (requirementsTxt ? resolvePyYamlFromRequirements(requirementsTxt) : null) ??
|
|
102
|
+
(pyprojectToml ? resolvePyYamlFromPyproject(pyprojectToml) : null);
|
|
103
|
+
const pyYamlSafeByDefault = pyYaml?.safeByDefault === true;
|
|
104
|
+
// Lazy: only split source lines when we actually need per-call
|
|
105
|
+
// inspection (i.e. gate D is active and could fire).
|
|
106
|
+
let sourceLines = null;
|
|
107
|
+
const getSourceLines = () => {
|
|
108
|
+
if (sourceLines === null)
|
|
109
|
+
sourceLines = code.split('\n');
|
|
110
|
+
return sourceLines;
|
|
111
|
+
};
|
|
79
112
|
let droppedFastjson = 0;
|
|
80
113
|
let droppedJackson = 0;
|
|
81
114
|
let droppedSnakeYaml = 0;
|
|
115
|
+
let droppedPyYaml = 0;
|
|
82
116
|
const kept = sinks.filter((sink) => {
|
|
83
117
|
if (sink.type !== 'deserialization')
|
|
84
118
|
return true;
|
|
@@ -107,16 +141,29 @@ export class DeserializationSafetyGatePass {
|
|
|
107
141
|
droppedSnakeYaml++;
|
|
108
142
|
return false;
|
|
109
143
|
}
|
|
144
|
+
// Gate D — PyYAML (Python). `sink.class` may be `undefined` for
|
|
145
|
+
// Python: unlike Java, Python calls do not carry receiver-type
|
|
146
|
+
// resolution into the CallInfo, so the taint-matcher's sinkMap
|
|
147
|
+
// assigns undefined for the class field. Accept undefined OR
|
|
148
|
+
// exact-`'yaml'` (mirrors Gate A's Fastjson check).
|
|
149
|
+
if (language === 'python' &&
|
|
150
|
+
pyYamlSafeByDefault &&
|
|
151
|
+
PYYAML_METHODS.has(sink.method) &&
|
|
152
|
+
(sink.class === undefined || PYYAML_CLASSES.has(sink.class)) &&
|
|
153
|
+
!fileHasUnsafePyYamlLoader(getSourceLines(), sink.line)) {
|
|
154
|
+
droppedPyYaml++;
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
110
157
|
return true;
|
|
111
158
|
});
|
|
112
|
-
const totalDropped = droppedFastjson + droppedJackson + droppedSnakeYaml;
|
|
159
|
+
const totalDropped = droppedFastjson + droppedJackson + droppedSnakeYaml + droppedPyYaml;
|
|
113
160
|
if (totalDropped > 0) {
|
|
114
161
|
// Mutate in place so downstream passes see the reduced sink set.
|
|
115
162
|
// Matches SinkSemanticsPass's contract.
|
|
116
163
|
sinks.length = 0;
|
|
117
164
|
sinks.push(...kept);
|
|
118
165
|
}
|
|
119
|
-
return { droppedFastjson, droppedJackson, droppedSnakeYaml };
|
|
166
|
+
return { droppedFastjson, droppedJackson, droppedSnakeYaml, droppedPyYaml };
|
|
120
167
|
}
|
|
121
168
|
}
|
|
122
169
|
//# sourceMappingURL=deserialization-safety-gate-pass.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deserialization-safety-gate-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/deserialization-safety-gate-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAMH,OAAO,EACL,sBAAsB,EACtB,6BAA6B,EAC7B,8BAA8B,EAC9B,6BAA6B,GAC9B,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"deserialization-safety-gate-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/deserialization-safety-gate-pass.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PythonReceiverTaintFormatPass — cognium-dev #264 (Python receiver-taint).
|
|
3
|
+
*
|
|
4
|
+
* Emits `format_string` (CWE-134) findings for Python `.format(...)`
|
|
5
|
+
* calls whose RECEIVER is tainted. The pre-#264-Python-slice sink
|
|
6
|
+
* patterns key exclusively on `arg_positions`, which cannot represent
|
|
7
|
+
* a taint that lives on the call's receiver — Python's
|
|
8
|
+
* `str.format(*args)` shape puts the format template AS the receiver,
|
|
9
|
+
* not as an argument. Same is true of the `%`-operator shape
|
|
10
|
+
* (`taintedFmt % args`), which lives entirely in a binary-operator
|
|
11
|
+
* AST node, not a call.
|
|
12
|
+
*
|
|
13
|
+
* This pass takes the receiver-taint half of that gap by scanning
|
|
14
|
+
* `graph.ir.calls` for `.format(...)` calls whose receiver appears in
|
|
15
|
+
* the constant-propagation `tainted` set. On a hit, emits a direct
|
|
16
|
+
* `format_string` finding at the call site (severity `medium`, level
|
|
17
|
+
* `warning`).
|
|
18
|
+
*
|
|
19
|
+
* The `%`-operator half is genuinely bigger — needs a new AST scan
|
|
20
|
+
* over `binary_operator` nodes with format-shape RHS detection — and
|
|
21
|
+
* remains deferred on #264.
|
|
22
|
+
*
|
|
23
|
+
* Python-only. No-op on other languages.
|
|
24
|
+
*
|
|
25
|
+
* Pipeline slot: runs after `ConstantPropagationPass` so the
|
|
26
|
+
* `tainted` set is populated. Any pass position after
|
|
27
|
+
* `constant-propagation` is safe; placing near the sink-emission
|
|
28
|
+
* chain keeps like-with-like grouped.
|
|
29
|
+
*
|
|
30
|
+
* Direct-finding emission (via `ctx.addFinding`) rather than
|
|
31
|
+
* synthetic-sink emission because the format-string vulnerability
|
|
32
|
+
* here doesn't need a taint-source → sink flow trace: the
|
|
33
|
+
* `constant-propagation.tainted` set implicitly encodes that the
|
|
34
|
+
* receiver traces back to a source. Emitting a sink would require
|
|
35
|
+
* TaintPropagationPass to also understand receiver-taint, which
|
|
36
|
+
* would be a much broader engine change.
|
|
37
|
+
*/
|
|
38
|
+
import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
|
|
39
|
+
export interface PythonReceiverTaintFormatResult {
|
|
40
|
+
/** Number of `.format()` calls flagged this run. */
|
|
41
|
+
findingsEmitted: number;
|
|
42
|
+
}
|
|
43
|
+
export declare class PythonReceiverTaintFormatPass implements AnalysisPass<PythonReceiverTaintFormatResult> {
|
|
44
|
+
readonly name = "python-receiver-taint-format";
|
|
45
|
+
readonly category: "security";
|
|
46
|
+
run(ctx: PassContext): PythonReceiverTaintFormatResult;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=python-receiver-taint-format-pass.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"python-receiver-taint-format-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/python-receiver-taint-format-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAG9E,MAAM,WAAW,+BAA+B;IAC9C,oDAAoD;IACpD,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,6BACX,YAAW,YAAY,CAAC,+BAA+B,CAAC;IAExD,QAAQ,CAAC,IAAI,kCAAkC;IAC/C,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,+BAA+B;CAsDvD"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PythonReceiverTaintFormatPass — cognium-dev #264 (Python receiver-taint).
|
|
3
|
+
*
|
|
4
|
+
* Emits `format_string` (CWE-134) findings for Python `.format(...)`
|
|
5
|
+
* calls whose RECEIVER is tainted. The pre-#264-Python-slice sink
|
|
6
|
+
* patterns key exclusively on `arg_positions`, which cannot represent
|
|
7
|
+
* a taint that lives on the call's receiver — Python's
|
|
8
|
+
* `str.format(*args)` shape puts the format template AS the receiver,
|
|
9
|
+
* not as an argument. Same is true of the `%`-operator shape
|
|
10
|
+
* (`taintedFmt % args`), which lives entirely in a binary-operator
|
|
11
|
+
* AST node, not a call.
|
|
12
|
+
*
|
|
13
|
+
* This pass takes the receiver-taint half of that gap by scanning
|
|
14
|
+
* `graph.ir.calls` for `.format(...)` calls whose receiver appears in
|
|
15
|
+
* the constant-propagation `tainted` set. On a hit, emits a direct
|
|
16
|
+
* `format_string` finding at the call site (severity `medium`, level
|
|
17
|
+
* `warning`).
|
|
18
|
+
*
|
|
19
|
+
* The `%`-operator half is genuinely bigger — needs a new AST scan
|
|
20
|
+
* over `binary_operator` nodes with format-shape RHS detection — and
|
|
21
|
+
* remains deferred on #264.
|
|
22
|
+
*
|
|
23
|
+
* Python-only. No-op on other languages.
|
|
24
|
+
*
|
|
25
|
+
* Pipeline slot: runs after `ConstantPropagationPass` so the
|
|
26
|
+
* `tainted` set is populated. Any pass position after
|
|
27
|
+
* `constant-propagation` is safe; placing near the sink-emission
|
|
28
|
+
* chain keeps like-with-like grouped.
|
|
29
|
+
*
|
|
30
|
+
* Direct-finding emission (via `ctx.addFinding`) rather than
|
|
31
|
+
* synthetic-sink emission because the format-string vulnerability
|
|
32
|
+
* here doesn't need a taint-source → sink flow trace: the
|
|
33
|
+
* `constant-propagation.tainted` set implicitly encodes that the
|
|
34
|
+
* receiver traces back to a source. Emitting a sink would require
|
|
35
|
+
* TaintPropagationPass to also understand receiver-taint, which
|
|
36
|
+
* would be a much broader engine change.
|
|
37
|
+
*/
|
|
38
|
+
export class PythonReceiverTaintFormatPass {
|
|
39
|
+
name = 'python-receiver-taint-format';
|
|
40
|
+
category = 'security';
|
|
41
|
+
run(ctx) {
|
|
42
|
+
if (ctx.language !== 'python') {
|
|
43
|
+
return { findingsEmitted: 0 };
|
|
44
|
+
}
|
|
45
|
+
const { calls, meta } = ctx.graph.ir;
|
|
46
|
+
const constProp = ctx.getResult('constant-propagation');
|
|
47
|
+
const tainted = constProp.tainted;
|
|
48
|
+
let count = 0;
|
|
49
|
+
for (const call of calls) {
|
|
50
|
+
if (call.method_name !== 'format')
|
|
51
|
+
continue;
|
|
52
|
+
const receiver = call.receiver;
|
|
53
|
+
if (!receiver)
|
|
54
|
+
continue;
|
|
55
|
+
// Only bare-identifier receivers can be looked up in the tainted
|
|
56
|
+
// set. Skip complex receivers (`obj.attr.format()`, `f(x).format()`)
|
|
57
|
+
// — those don't correspond to a variable name the taint tracker
|
|
58
|
+
// would have on file. Recall may improve later with attribute-
|
|
59
|
+
// chain resolution, but the direct-identifier form is the common
|
|
60
|
+
// shape for user-controlled templates.
|
|
61
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(receiver))
|
|
62
|
+
continue;
|
|
63
|
+
// constant-propagation stores both scoped (`method:var`) and
|
|
64
|
+
// unscoped forms in the tainted set. Check both.
|
|
65
|
+
const scopedName = call.in_method
|
|
66
|
+
? `${call.in_method}:${receiver}`
|
|
67
|
+
: receiver;
|
|
68
|
+
if (!tainted.has(receiver) && !tainted.has(scopedName))
|
|
69
|
+
continue;
|
|
70
|
+
ctx.addFinding({
|
|
71
|
+
id: `format_string-${meta.file}-${call.location.line}-${receiver}`,
|
|
72
|
+
pass: this.name,
|
|
73
|
+
category: 'security',
|
|
74
|
+
rule_id: 'format_string',
|
|
75
|
+
cwe: 'CWE-134',
|
|
76
|
+
severity: 'medium',
|
|
77
|
+
level: 'warning',
|
|
78
|
+
message: `Format-string vulnerability: the format template \`${receiver}\` ` +
|
|
79
|
+
`is tainted (traces back to a taint source). Attacker can leak ` +
|
|
80
|
+
`object attributes via \`{obj.__class__.__mro__[…]}\` chains or ` +
|
|
81
|
+
`crash the process via unbounded specifiers. Use a literal ` +
|
|
82
|
+
`format string and pass user input as an argument instead.`,
|
|
83
|
+
file: meta.file,
|
|
84
|
+
line: call.location.line,
|
|
85
|
+
});
|
|
86
|
+
count++;
|
|
87
|
+
}
|
|
88
|
+
return { findingsEmitted: count };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=python-receiver-taint-format-pass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"python-receiver-taint-format-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/python-receiver-taint-format-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAUH,MAAM,OAAO,6BAA6B;IAG/B,IAAI,GAAG,8BAA8B,CAAC;IACtC,QAAQ,GAAG,UAAmB,CAAC;IAExC,GAAG,CAAC,GAAgB;QAClB,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC9B,OAAO,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC;QAChC,CAAC;QAED,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,CAC7B,sBAAsB,CACvB,CAAC;QACF,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC;QAElC,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,WAAW,KAAK,QAAQ;gBAAE,SAAS;YAC5C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;YAC/B,IAAI,CAAC,QAAQ;gBAAE,SAAS;YAExB,iEAAiE;YACjE,qEAAqE;YACrE,gEAAgE;YAChE,+DAA+D;YAC/D,iEAAiE;YACjE,uCAAuC;YACvC,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAEzD,6DAA6D;YAC7D,iDAAiD;YACjD,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS;gBAC/B,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,IAAI,QAAQ,EAAE;gBACjC,CAAC,CAAC,QAAQ,CAAC;YACb,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;gBAAE,SAAS;YAEjE,GAAG,CAAC,UAAU,CAAC;gBACb,EAAE,EAAE,iBAAiB,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,QAAQ,EAAE;gBAClE,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,QAAQ,EAAE,UAAU;gBACpB,OAAO,EAAE,eAAe;gBACxB,GAAG,EAAE,SAAS;gBACd,QAAQ,EAAE,QAAQ;gBAClB,KAAK,EAAE,SAAS;gBAChB,OAAO,EACL,sDAAsD,QAAQ,KAAK;oBACnE,gEAAgE;oBAChE,iEAAiE;oBACjE,4DAA4D;oBAC5D,2DAA2D;gBAC7D,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI;aACzB,CAAC,CAAC;YACH,KAAK,EAAE,CAAC;QACV,CAAC;QAED,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;IACpC,CAAC;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"taint-matcher.d.ts","sourceRoot":"","sources":["../../src/analysis/taint-matcher.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAgB,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAkB,KAAK,EAAwB,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAClK,OAAO,KAAK,EAAE,WAAW,EAAiB,WAAW,EAAoB,MAAM,oBAAoB,CAAC;AACpG,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAgC7E;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,QAAQ,EAAE,EACjB,KAAK,EAAE,QAAQ,EAAE,EACjB,MAAM,GAAE,WAAgC,EACxC,aAAa,CAAC,EAAE,qBAAqB,EACrC,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,IAAI,CAAC,EAAE,MAAM,GACZ,KAAK,CASP;AAqID;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,WAAW,EAAE,EACtB,KAAK,EAAE,SAAS,EAAE,EAClB,IAAI,EAAE,MAAM,GACX,IAAI,CAYN;
|
|
1
|
+
{"version":3,"file":"taint-matcher.d.ts","sourceRoot":"","sources":["../../src/analysis/taint-matcher.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAgB,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAkB,KAAK,EAAwB,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAClK,OAAO,KAAK,EAAE,WAAW,EAAiB,WAAW,EAAoB,MAAM,oBAAoB,CAAC;AACpG,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAgC7E;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,QAAQ,EAAE,EACjB,KAAK,EAAE,QAAQ,EAAE,EACjB,MAAM,GAAE,WAAgC,EACxC,aAAa,CAAC,EAAE,qBAAqB,EACrC,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,IAAI,CAAC,EAAE,MAAM,GACZ,KAAK,CASP;AAqID;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,WAAW,EAAE,EACtB,KAAK,EAAE,SAAS,EAAE,EAClB,IAAI,EAAE,MAAM,GACX,IAAI,CAYN;AA43ED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,WAAW,GACnB,OAAO,CAET"}
|
|
@@ -2407,6 +2407,24 @@ function calculateSinkConfidence(call, pattern) {
|
|
|
2407
2407
|
if (pattern.class && call.receiver) {
|
|
2408
2408
|
if (receiverMightBeClass(call.receiver, pattern.class)) {
|
|
2409
2409
|
confidence += 0.1;
|
|
2410
|
+
// cognium-dev #260 — EXACT-match tiebreaker. When two patterns
|
|
2411
|
+
// for the same `(method, cwe)` both accept the receiver — one
|
|
2412
|
+
// exactly (e.g. class: 'Ctx' vs receiver 'Ctx') and one via
|
|
2413
|
+
// `receiverMightBeClass` fuzzy heuristics (e.g. class: 'Context'
|
|
2414
|
+
// vs receiver 'Ctx' via the includes/40%-length rule at
|
|
2415
|
+
// taint-matcher.ts:2366) — the fuzzy match used to win the
|
|
2416
|
+
// `sinkMap` dedup slot when it iterated first (both had the same
|
|
2417
|
+
// 0.9 confidence). The extra boost here guarantees the exact-
|
|
2418
|
+
// match pattern's `arg_positions` are the ones emitted on the
|
|
2419
|
+
// final sink. Fixes the fiber `c.Redirect(next)` shape whose
|
|
2420
|
+
// sink was emitted with the gin `class: 'Context'` pattern's
|
|
2421
|
+
// `arg_positions: [1]` instead of fiber `class: 'Ctx'`'s
|
|
2422
|
+
// `arg_positions: [0]`, dropping the taint flow silently.
|
|
2423
|
+
const receiverLc = call.receiver.toLowerCase();
|
|
2424
|
+
const classLc = pattern.class.toLowerCase();
|
|
2425
|
+
if (call.receiver === pattern.class || receiverLc === classLc) {
|
|
2426
|
+
confidence += 0.05;
|
|
2427
|
+
}
|
|
2410
2428
|
}
|
|
2411
2429
|
}
|
|
2412
2430
|
// Higher confidence for critical severity patterns
|