circle-ir 3.155.0 → 3.157.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.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * MyBatisAnnotationSqlSinkPass — cognium-dev #241 Java
3
+ *
4
+ * Detects SQL injection sinks on MyBatis annotation-driven Mapper interface
5
+ * methods. MyBatis distinguishes two placeholder syntaxes in `@Select` /
6
+ * `@Update` / `@Insert` / `@Delete` (and their `*Provider` variants):
7
+ *
8
+ * - `#{name}` — JDBC PreparedStatement parameter binding. SAFE.
9
+ * - `${name}` — raw string interpolation into the SQL string. **SQLi.**
10
+ *
11
+ * The taint-matcher's YAML sink registry (`configs/sinks/sql.yaml`) covers
12
+ * standard Mapper method names (insert/update/select-wildcard/delete) via a
13
+ * generic `mybatis_mapper_call` discovery marker, but cannot inspect the
14
+ * contents of an annotation string. Custom method names (`findByName`,
15
+ * `getUserById`) with `${}` interpolation therefore fall through both the
16
+ * name-based Mapper matcher and any downstream SQLi flow generator.
17
+ *
18
+ * This pass closes that gap. For every Java interface whose method carries
19
+ * a MyBatis SQL annotation containing at least one `${varname}`, the pass
20
+ * records the tainted parameter positions (correlated to `@Param("name")`
21
+ * annotations or MyBatis positional convention `${param1}` / `${0}`), then
22
+ * walks `graph.calls` for call sites targeting those Mapper methods and
23
+ * emits a synthetic `sql_injection` (CWE-89) `TaintSink` at each match.
24
+ *
25
+ * The synthetic sinks are pushed onto the `TaintMatcherResult.sinks` list
26
+ * so `SinkFilterPass` (which merges taint-matcher + language-sources) and
27
+ * downstream flow generators pick them up without any registry changes.
28
+ *
29
+ * Pipeline slot: runs after `LanguageSourcesPass` and before
30
+ * `SinkFilterPass` so the synthetic sinks are visible to the four-stage
31
+ * FP-elimination filter and to `TaintPropagationPass` / `InterproceduralPass`.
32
+ *
33
+ * Language scope: Java only. Non-Java files short-circuit at run().
34
+ *
35
+ * Kill switch: opt out via `disabledPasses: ['mybatis-annotation-sql-sink']`.
36
+ */
37
+ import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
38
+ export interface MyBatisAnnotationSqlSinkResult {
39
+ /** Number of annotated Mapper methods with `${}` interpolation found. */
40
+ annotatedMethodCount: number;
41
+ /** Number of synthetic `sql_injection` sinks added. */
42
+ addedSinkCount: number;
43
+ }
44
+ export declare class MyBatisAnnotationSqlSinkPass implements AnalysisPass<MyBatisAnnotationSqlSinkResult> {
45
+ readonly name = "mybatis-annotation-sql-sink";
46
+ readonly category: "security";
47
+ run(ctx: PassContext): MyBatisAnnotationSqlSinkResult;
48
+ }
49
+ //# sourceMappingURL=mybatis-annotation-sql-sink-pass.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mybatis-annotation-sql-sink-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/mybatis-annotation-sql-sink-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAG9E,MAAM,WAAW,8BAA8B;IAC7C,yEAAyE;IACzE,oBAAoB,EAAE,MAAM,CAAC;IAC7B,uDAAuD;IACvD,cAAc,EAAE,MAAM,CAAC;CACxB;AAqND,qBAAa,4BACX,YAAW,YAAY,CAAC,8BAA8B,CAAC;IAEvD,QAAQ,CAAC,IAAI,iCAAiC;IAC9C,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,8BAA8B;CA0HtD"}
@@ -0,0 +1,329 @@
1
+ /**
2
+ * MyBatisAnnotationSqlSinkPass — cognium-dev #241 Java
3
+ *
4
+ * Detects SQL injection sinks on MyBatis annotation-driven Mapper interface
5
+ * methods. MyBatis distinguishes two placeholder syntaxes in `@Select` /
6
+ * `@Update` / `@Insert` / `@Delete` (and their `*Provider` variants):
7
+ *
8
+ * - `#{name}` — JDBC PreparedStatement parameter binding. SAFE.
9
+ * - `${name}` — raw string interpolation into the SQL string. **SQLi.**
10
+ *
11
+ * The taint-matcher's YAML sink registry (`configs/sinks/sql.yaml`) covers
12
+ * standard Mapper method names (insert/update/select-wildcard/delete) via a
13
+ * generic `mybatis_mapper_call` discovery marker, but cannot inspect the
14
+ * contents of an annotation string. Custom method names (`findByName`,
15
+ * `getUserById`) with `${}` interpolation therefore fall through both the
16
+ * name-based Mapper matcher and any downstream SQLi flow generator.
17
+ *
18
+ * This pass closes that gap. For every Java interface whose method carries
19
+ * a MyBatis SQL annotation containing at least one `${varname}`, the pass
20
+ * records the tainted parameter positions (correlated to `@Param("name")`
21
+ * annotations or MyBatis positional convention `${param1}` / `${0}`), then
22
+ * walks `graph.calls` for call sites targeting those Mapper methods and
23
+ * emits a synthetic `sql_injection` (CWE-89) `TaintSink` at each match.
24
+ *
25
+ * The synthetic sinks are pushed onto the `TaintMatcherResult.sinks` list
26
+ * so `SinkFilterPass` (which merges taint-matcher + language-sources) and
27
+ * downstream flow generators pick them up without any registry changes.
28
+ *
29
+ * Pipeline slot: runs after `LanguageSourcesPass` and before
30
+ * `SinkFilterPass` so the synthetic sinks are visible to the four-stage
31
+ * FP-elimination filter and to `TaintPropagationPass` / `InterproceduralPass`.
32
+ *
33
+ * Language scope: Java only. Non-Java files short-circuit at run().
34
+ *
35
+ * Kill switch: opt out via `disabledPasses: ['mybatis-annotation-sql-sink']`.
36
+ */
37
+ /**
38
+ * MyBatis SQL annotations. The `*Provider` variants take a
39
+ * `type = X.class, method = "y"` pair whose provider method returns the
40
+ * SQL string; we still scan the annotation-attached literal (if any) for
41
+ * `${}` markers as a best-effort recall bump.
42
+ */
43
+ const MYBATIS_SQL_ANNOTATIONS = new Set([
44
+ 'Select',
45
+ 'Update',
46
+ 'Insert',
47
+ 'Delete',
48
+ 'SelectProvider',
49
+ 'UpdateProvider',
50
+ 'InsertProvider',
51
+ 'DeleteProvider',
52
+ ]);
53
+ /**
54
+ * Regex to extract `${varname}` interpolation markers from an annotation
55
+ * string. Deliberately does NOT match `#{name}` (safe binding).
56
+ * Variable names follow the MyBatis convention (identifier + optional
57
+ * dotted property access, e.g. `${user.id}`) — we only use the leading
58
+ * identifier when correlating to `@Param`.
59
+ */
60
+ const DOLLAR_BRACE_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?:\.[A-Za-z0-9_.]+)?\}/g;
61
+ /**
62
+ * MyBatis positional convention: `${param1}` refers to the first argument
63
+ * (1-based). `${0}` — some MyBatis versions — refers to the first argument
64
+ * (0-based). We map both forms to a 0-based index.
65
+ */
66
+ function parsePositionalRef(name) {
67
+ const paramMatch = /^param(\d+)$/.exec(name);
68
+ if (paramMatch) {
69
+ const oneBased = Number.parseInt(paramMatch[1], 10);
70
+ if (Number.isFinite(oneBased) && oneBased >= 1)
71
+ return oneBased - 1;
72
+ }
73
+ const zeroMatch = /^(\d+)$/.exec(name);
74
+ if (zeroMatch) {
75
+ const zeroBased = Number.parseInt(zeroMatch[1], 10);
76
+ if (Number.isFinite(zeroBased) && zeroBased >= 0)
77
+ return zeroBased;
78
+ }
79
+ return null;
80
+ }
81
+ /**
82
+ * Extract the raw annotation-argument string from a stored annotation.
83
+ * Annotations are stored as e.g. `Select("SELECT ... ${x} ...")` (without
84
+ * the leading `@`). We return the substring between the outermost
85
+ * parentheses, or `null` for a marker annotation with no arguments.
86
+ */
87
+ function extractAnnotationBody(annotation) {
88
+ const openIdx = annotation.indexOf('(');
89
+ if (openIdx < 0)
90
+ return null;
91
+ const closeIdx = annotation.lastIndexOf(')');
92
+ if (closeIdx <= openIdx)
93
+ return null;
94
+ return annotation.substring(openIdx + 1, closeIdx);
95
+ }
96
+ /**
97
+ * Given a stored annotation `Foo(...)`, return the leading identifier
98
+ * `Foo`. For marker annotations without parens, returns the whole string.
99
+ */
100
+ function annotationName(annotation) {
101
+ const openIdx = annotation.indexOf('(');
102
+ return openIdx < 0 ? annotation : annotation.substring(0, openIdx);
103
+ }
104
+ /**
105
+ * Return the value of the first string argument to a `@Param("name")`
106
+ * annotation, or `null` if the annotation is not `@Param` or has no
107
+ * literal string argument.
108
+ */
109
+ function extractParamName(annotation) {
110
+ if (annotationName(annotation) !== 'Param')
111
+ return null;
112
+ const body = extractAnnotationBody(annotation);
113
+ if (body === null)
114
+ return null;
115
+ // Match "..." or '...' at the start of the body. MyBatis @Param only
116
+ // takes a single string literal, so a simple match is sufficient.
117
+ const strMatch = /^\s*["']([^"']*)["']/.exec(body);
118
+ return strMatch ? strMatch[1] : null;
119
+ }
120
+ /**
121
+ * Extract every `${varname}` reference from a MyBatis annotation body
122
+ * (typically a single quoted string literal, sometimes concatenated).
123
+ * Returns the deduplicated ordered list of leading identifiers.
124
+ */
125
+ function extractDollarBraceRefs(annotationBody) {
126
+ const seen = new Set();
127
+ const out = [];
128
+ DOLLAR_BRACE_RE.lastIndex = 0;
129
+ let m;
130
+ while ((m = DOLLAR_BRACE_RE.exec(annotationBody)) !== null) {
131
+ const name = m[1];
132
+ if (!seen.has(name)) {
133
+ seen.add(name);
134
+ out.push(name);
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+ /**
140
+ * Check if the file imports MyBatis annotations, either directly or via
141
+ * wildcard. Used to distinguish MyBatis `@Select` from unrelated libraries
142
+ * (e.g. Reactor's `@Select`, custom application `@Select`) that happen to
143
+ * share the simple annotation name.
144
+ */
145
+ function fileImportsMyBatis(imports) {
146
+ for (const imp of imports) {
147
+ const pkg = imp.from_package;
148
+ if (!pkg)
149
+ continue;
150
+ if (pkg === 'org.apache.ibatis.annotations' ||
151
+ pkg.startsWith('org.apache.ibatis.annotations.') ||
152
+ pkg === 'org.apache.ibatis' ||
153
+ pkg.startsWith('org.apache.ibatis.')) {
154
+ return true;
155
+ }
156
+ }
157
+ return false;
158
+ }
159
+ /**
160
+ * Correlate every `${name}` reference to a 0-based parameter index. When
161
+ * the method carries `@Param("name")` on any parameter, that mapping wins.
162
+ * Otherwise MyBatis positional convention is used (`${param1}`, `${0}`).
163
+ * Returns the deduplicated ascending list of tainted arg positions.
164
+ */
165
+ function correlateDollarBraceToArgPositions(method, refs) {
166
+ const paramNameToIndex = new Map();
167
+ method.parameters.forEach((param, idx) => {
168
+ for (const ann of param.annotations) {
169
+ const paramName = extractParamName(ann);
170
+ if (paramName !== null) {
171
+ paramNameToIndex.set(paramName, idx);
172
+ }
173
+ }
174
+ });
175
+ const positions = new Set();
176
+ for (const ref of refs) {
177
+ const namedIdx = paramNameToIndex.get(ref);
178
+ if (namedIdx !== undefined) {
179
+ positions.add(namedIdx);
180
+ continue;
181
+ }
182
+ const positionalIdx = parsePositionalRef(ref);
183
+ if (positionalIdx !== null &&
184
+ positionalIdx < method.parameters.length) {
185
+ positions.add(positionalIdx);
186
+ }
187
+ }
188
+ return Array.from(positions).sort((a, b) => a - b);
189
+ }
190
+ /**
191
+ * Return the fully-qualified name of a Mapper interface method for use as
192
+ * a callee lookup key. Falls back to `<simpleName>.<methodName>` when the
193
+ * interface has no declared package.
194
+ */
195
+ function mapperMethodKey(type, method) {
196
+ const iface = type.package ? `${type.package}.${type.name}` : type.name;
197
+ return `${iface}.${method.name}`;
198
+ }
199
+ /**
200
+ * Match a call site against a Mapper interface method by callee tail. We
201
+ * accept:
202
+ * - `resolution.target` equal to or ending with the mapper key
203
+ * - simple `method_name` equal to the mapper's method AND
204
+ * `receiver_type` or `receiver_type_fqn` equal to the mapper's
205
+ * interface (simple- or fully-qualified name).
206
+ *
207
+ * Conservative on both sides — unresolved receivers fall through.
208
+ */
209
+ function isMapperMethodCall(call, interfaceSimpleName, interfaceFqn, methodName) {
210
+ // Path 1 — resolution target tail match.
211
+ const target = call.resolution?.target;
212
+ if (target) {
213
+ const suffix = `${interfaceSimpleName}.${methodName}`;
214
+ const suffixFqn = `${interfaceFqn}.${methodName}`;
215
+ if (target === suffix ||
216
+ target === suffixFqn ||
217
+ target.endsWith('.' + suffix) ||
218
+ target.endsWith('.' + suffixFqn)) {
219
+ return true;
220
+ }
221
+ }
222
+ // Path 2 — method name + receiver type match.
223
+ if (call.method_name !== methodName)
224
+ return false;
225
+ if (call.receiver_type === interfaceSimpleName)
226
+ return true;
227
+ if (call.receiver_type_fqn === interfaceFqn)
228
+ return true;
229
+ return false;
230
+ }
231
+ export class MyBatisAnnotationSqlSinkPass {
232
+ name = 'mybatis-annotation-sql-sink';
233
+ category = 'security';
234
+ run(ctx) {
235
+ if (ctx.language !== 'java') {
236
+ return { annotatedMethodCount: 0, addedSinkCount: 0 };
237
+ }
238
+ const { types, imports, calls } = ctx.graph.ir;
239
+ // Gate on file-level MyBatis import to distinguish real Mapper
240
+ // interfaces from unrelated libraries reusing the `@Select` name.
241
+ if (!fileImportsMyBatis(imports)) {
242
+ return { annotatedMethodCount: 0, addedSinkCount: 0 };
243
+ }
244
+ const mapperMethods = [];
245
+ for (const type of types) {
246
+ if (type.kind !== 'interface')
247
+ continue;
248
+ for (const method of type.methods) {
249
+ // Find MyBatis SQL annotation, extract body, scan for `${}`.
250
+ let matchedRefs = [];
251
+ for (const ann of method.annotations) {
252
+ if (!MYBATIS_SQL_ANNOTATIONS.has(annotationName(ann)))
253
+ continue;
254
+ const body = extractAnnotationBody(ann);
255
+ if (body === null)
256
+ continue;
257
+ const refs = extractDollarBraceRefs(body);
258
+ if (refs.length > 0) {
259
+ matchedRefs = [...matchedRefs, ...refs];
260
+ }
261
+ }
262
+ if (matchedRefs.length === 0)
263
+ continue;
264
+ const positions = correlateDollarBraceToArgPositions(method, matchedRefs);
265
+ if (positions.length === 0)
266
+ continue;
267
+ const interfaceFqn = type.package
268
+ ? `${type.package}.${type.name}`
269
+ : type.name;
270
+ mapperMethods.push({
271
+ interfaceSimpleName: type.name,
272
+ interfaceFqn,
273
+ methodName: method.name,
274
+ taintedArgPositions: positions,
275
+ });
276
+ }
277
+ }
278
+ if (mapperMethods.length === 0) {
279
+ return {
280
+ annotatedMethodCount: 0,
281
+ addedSinkCount: 0,
282
+ };
283
+ }
284
+ // Grab the taint-matcher sinks array to append into. When the pass is
285
+ // run stand-alone (unit-test harness) without TaintMatcherPass, we
286
+ // fall back to graph.ir.taint.sinks so tests can still observe the
287
+ // added sinks.
288
+ const sinks = ctx.hasResult('taint-matcher')
289
+ ? ctx.getResult('taint-matcher').sinks
290
+ : ctx.graph.ir.taint.sinks;
291
+ let addedSinkCount = 0;
292
+ for (const call of calls) {
293
+ for (const rec of mapperMethods) {
294
+ if (!isMapperMethodCall(call, rec.interfaceSimpleName, rec.interfaceFqn, rec.methodName)) {
295
+ continue;
296
+ }
297
+ // Dedup: skip if a `sql_injection` sink already exists at this
298
+ // call site.
299
+ const line = call.location.line;
300
+ if (sinks.some((s) => s.line === line &&
301
+ s.type === 'sql_injection' &&
302
+ s.method === rec.methodName)) {
303
+ continue;
304
+ }
305
+ const receiverLoc = call.receiver
306
+ ? `${call.receiver}.${rec.methodName}()`
307
+ : `${rec.methodName}()`;
308
+ sinks.push({
309
+ type: 'sql_injection',
310
+ cwe: 'CWE-89',
311
+ location: call.in_method
312
+ ? `${receiverLoc} in ${call.in_method}`
313
+ : receiverLoc,
314
+ line,
315
+ confidence: 0.95,
316
+ method: rec.methodName,
317
+ argPositions: rec.taintedArgPositions,
318
+ class: rec.interfaceSimpleName,
319
+ });
320
+ addedSinkCount++;
321
+ }
322
+ }
323
+ return {
324
+ annotatedMethodCount: mapperMethods.length,
325
+ addedSinkCount,
326
+ };
327
+ }
328
+ }
329
+ //# sourceMappingURL=mybatis-annotation-sql-sink-pass.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mybatis-annotation-sql-sink-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/mybatis-annotation-sql-sink-pass.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"taint-propagation-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/taint-propagation-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAQ9E,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,aAAa,EAAE,CAAC;CACxB;AAED,qBAAa,oBAAqB,YAAW,YAAY,CAAC,0BAA0B,CAAC;IACnF,QAAQ,CAAC,IAAI,uBAAuB;IACpC,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,0BAA0B;CA2TlD"}
1
+ {"version":3,"file":"taint-propagation-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/taint-propagation-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAS9E,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,aAAa,EAAE,CAAC;CACxB;AAED,qBAAa,oBAAqB,YAAW,YAAY,CAAC,0BAA0B,CAAC;IACnF,QAAQ,CAAC,IAAI,uBAAuB;IACpC,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,0BAA0B;CAkWlD"}
@@ -14,6 +14,7 @@ import { propagateTaint } from '../taint-propagation.js';
14
14
  import { isFalsePositive, isCorrelatedPredicateFP } from '../constant-propagation.js';
15
15
  import { buildJavaTaintedVars, buildPythonTaintedVars, buildRustTaintedVars } from './language-sources-pass.js';
16
16
  import { canSourceReachSink, sourceSemanticsAllowed } from '../findings.js';
17
+ import { walkBackwardDefs } from '../dfg-walk.js';
17
18
  export class TaintPropagationPass {
18
19
  name = 'taint-propagation';
19
20
  category = 'security';
@@ -211,11 +212,50 @@ export class TaintPropagationPass {
211
212
  return true;
212
213
  }
213
214
  const sansAtSink = sanitizersByLine.get(f.sink_line);
214
- if (!sansAtSink || sansAtSink.length === 0)
215
+ if (sansAtSink && sansAtSink.length > 0) {
216
+ for (const san of sansAtSink) {
217
+ if (san.sanitizes.includes(f.sink_type)) {
218
+ return false;
219
+ }
220
+ }
221
+ }
222
+ // cognium-dev #239 C4 residual — DFG-walk sanitizer credit for
223
+ // configured sinks. Supplementary flow generators
224
+ // (detectExpressionScanFlows, detectCollectionFlows,
225
+ // detectParameterSinkFlows) emit sanitizer-blind flows. The
226
+ // sink-line fast path above misses the common `n = parseInt(x);
227
+ // sink(n);` shape where the sanitizer lives on the assignment
228
+ // line, not the sink line. Walk the reaching-def chain of the
229
+ // sink's tainted use and credit sanitizers on any def line in
230
+ // the chain. Variable-precise — mirrors site #2 walkback logic
231
+ // in taint-propagation.ts checkSanitized. Doesn't over-suppress
232
+ // bare `shlex.quote(host)` on unrelated non-sink lines because
233
+ // those calls don't create a def in the sink var's chain.
234
+ const usesAtSink = graph.usesByLine.get(f.sink_line);
235
+ if (!usesAtSink || usesAtSink.length === 0)
236
+ return true;
237
+ const sinkVar = f.path.length > 0
238
+ ? f.path[f.path.length - 1].variable
239
+ : undefined;
240
+ if (!sinkVar)
215
241
  return true;
216
- for (const san of sansAtSink) {
217
- if (san.sanitizes.includes(f.sink_type)) {
218
- return false;
242
+ for (const use of usesAtSink) {
243
+ if (use.variable !== sinkVar)
244
+ continue;
245
+ if (use.def_id === null || use.def_id === undefined)
246
+ continue;
247
+ const walk = walkBackwardDefs(use.def_id, graph.chainsByToDef, graph.defById, { maxHops: 32 });
248
+ for (const line of walk.lines) {
249
+ if (line === f.sink_line)
250
+ continue; // already checked above
251
+ const sansAtLine = sanitizersByLine.get(line);
252
+ if (!sansAtLine || sansAtLine.length === 0)
253
+ continue;
254
+ for (const san of sansAtLine) {
255
+ if (san.sanitizes.includes(f.sink_type)) {
256
+ return false;
257
+ }
258
+ }
219
259
  }
220
260
  }
221
261
  return true;