circle-ir 4.14.1 → 4.15.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.
Files changed (95) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +9 -2
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/const-table-lookup.d.ts +29 -0
  5. package/dist/analysis/const-table-lookup.d.ts.map +1 -0
  6. package/dist/analysis/const-table-lookup.js +145 -0
  7. package/dist/analysis/const-table-lookup.js.map +1 -0
  8. package/dist/analysis/findings.d.ts.map +1 -1
  9. package/dist/analysis/findings.js +15 -4
  10. package/dist/analysis/findings.js.map +1 -1
  11. package/dist/analysis/go-strings-replacer.d.ts +21 -0
  12. package/dist/analysis/go-strings-replacer.d.ts.map +1 -0
  13. package/dist/analysis/go-strings-replacer.js +145 -0
  14. package/dist/analysis/go-strings-replacer.js.map +1 -0
  15. package/dist/analysis/interprocedural.d.ts.map +1 -1
  16. package/dist/analysis/interprocedural.js +53 -0
  17. package/dist/analysis/interprocedural.js.map +1 -1
  18. package/dist/analysis/java-canonical-prefix.d.ts +42 -0
  19. package/dist/analysis/java-canonical-prefix.d.ts.map +1 -0
  20. package/dist/analysis/java-canonical-prefix.js +145 -0
  21. package/dist/analysis/java-canonical-prefix.js.map +1 -0
  22. package/dist/analysis/java-crlf-strip.d.ts +30 -0
  23. package/dist/analysis/java-crlf-strip.d.ts.map +1 -0
  24. package/dist/analysis/java-crlf-strip.js +195 -0
  25. package/dist/analysis/java-crlf-strip.js.map +1 -0
  26. package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts +2 -0
  27. package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts.map +1 -1
  28. package/dist/analysis/passes/cleartext-credential-transport-pass.js +64 -12
  29. package/dist/analysis/passes/cleartext-credential-transport-pass.js.map +1 -1
  30. package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
  31. package/dist/analysis/passes/cross-file-pass.js +43 -0
  32. package/dist/analysis/passes/cross-file-pass.js.map +1 -1
  33. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
  34. package/dist/analysis/passes/csharp-sanitizer-credit.js +18 -0
  35. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
  36. package/dist/analysis/passes/insecure-cookie-pass.d.ts +11 -4
  37. package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
  38. package/dist/analysis/passes/insecure-cookie-pass.js +30 -15
  39. package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
  40. package/dist/analysis/passes/interprocedural-pass.d.ts +28 -0
  41. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  42. package/dist/analysis/passes/interprocedural-pass.js +31 -3
  43. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  44. package/dist/analysis/passes/language-sources-pass.d.ts +41 -2
  45. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  46. package/dist/analysis/passes/language-sources-pass.js +894 -10
  47. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  48. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts +8 -0
  49. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
  50. package/dist/analysis/passes/param-source-flow-gate-pass.js +162 -6
  51. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
  52. package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
  53. package/dist/analysis/passes/scan-secrets-pass.js +24 -3
  54. package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
  55. package/dist/analysis/passes/sink-filter-pass.d.ts +1 -1
  56. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  57. package/dist/analysis/passes/sink-filter-pass.js +255 -25
  58. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  59. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  60. package/dist/analysis/passes/taint-propagation-pass.js +268 -5
  61. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  62. package/dist/analysis/scalar-derivation.d.ts +25 -0
  63. package/dist/analysis/scalar-derivation.d.ts.map +1 -0
  64. package/dist/analysis/scalar-derivation.js +196 -0
  65. package/dist/analysis/scalar-derivation.js.map +1 -0
  66. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  67. package/dist/analysis/taint-matcher.js +172 -8
  68. package/dist/analysis/taint-matcher.js.map +1 -1
  69. package/dist/analysis/taint-propagation.d.ts +9 -0
  70. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  71. package/dist/analysis/taint-propagation.js +260 -11
  72. package/dist/analysis/taint-propagation.js.map +1 -1
  73. package/dist/analyzer.d.ts.map +1 -1
  74. package/dist/analyzer.js +8 -1
  75. package/dist/analyzer.js.map +1 -1
  76. package/dist/browser/circle-ir.js +3403 -1225
  77. package/dist/core/circle-ir-core.cjs +598 -26
  78. package/dist/core/circle-ir-core.js +598 -26
  79. package/dist/core/extractors/calls.js +19 -0
  80. package/dist/core/extractors/calls.js.map +1 -1
  81. package/dist/core/extractors/dfg.js +240 -6
  82. package/dist/core/extractors/dfg.js.map +1 -1
  83. package/dist/graph/code-graph.d.ts +9 -0
  84. package/dist/graph/code-graph.d.ts.map +1 -1
  85. package/dist/graph/code-graph.js +11 -0
  86. package/dist/graph/code-graph.js.map +1 -1
  87. package/dist/languages/plugins/go.d.ts.map +1 -1
  88. package/dist/languages/plugins/go.js +19 -0
  89. package/dist/languages/plugins/go.js.map +1 -1
  90. package/dist/resolution/cross-file.d.ts +7 -0
  91. package/dist/resolution/cross-file.d.ts.map +1 -1
  92. package/dist/resolution/cross-file.js +10 -1
  93. package/dist/resolution/cross-file.js.map +1 -1
  94. package/dist/wasm/web-tree-sitter.wasm +0 -0
  95. package/package.json +3 -3
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Scalar-derivation taint barrier (#568, parent #349).
3
+ *
4
+ * A local whose whole initializer measures or tests a value — its length, a
5
+ * match count, a boolean from a regex test or a containment check — holds an
6
+ * integer or a boolean. That value carries the magnitude of the input, not
7
+ * its content, so it cannot deliver a string payload to an injection sink.
8
+ *
9
+ * This is a type rule, not sanitizer credit for the call: a string-returning
10
+ * matcher (`re.FindString(text)`, `text.match(re)`) still propagates taint.
11
+ * Numeric parses (`strconv.Atoi`, `parseInt`) are not covered here; a parsed
12
+ * number is attacker-chosen and stays tainted for size-sensitive consumers.
13
+ *
14
+ * The test reads the definition's own line. An initializer that is not a
15
+ * single complete expression on that line is not credited.
16
+ */
17
+ /** Go calls that return an int or a bool computed from their argument. */
18
+ const GO_SCALAR_FUNCS = new Set(['len', 'utf8.RuneCountInString', 'utf8.RuneCount']);
19
+ /** `strings.X` / `bytes.X` functions with an int or bool result. */
20
+ const GO_STRINGS_SCALAR = new Set([
21
+ 'Contains', 'ContainsAny', 'ContainsRune', 'HasPrefix', 'HasSuffix', 'EqualFold',
22
+ 'Count', 'Index', 'IndexAny', 'IndexByte', 'IndexRune', 'LastIndex', 'Compare',
23
+ ]);
24
+ /** `*regexp.Regexp` methods with a bool result, on any receiver. */
25
+ const GO_REGEXP_BOOL = new Set(['MatchString', 'Match']);
26
+ /**
27
+ * JS/TS members that yield a number or a boolean. `search` is left out: on
28
+ * search-index objects (`idx.search(q)`) it returns result data, not a number.
29
+ */
30
+ const JS_SCALAR_METHODS = new Set([
31
+ 'test', 'includes', 'startsWith', 'endsWith', 'indexOf', 'lastIndexOf',
32
+ ]);
33
+ function scalarLanguage(language) {
34
+ return language === 'go' || language === 'javascript' || language === 'typescript' ? language : null;
35
+ }
36
+ /**
37
+ * Split `expr` at depth 0 into its trailing segment. Returns null when the
38
+ * expression is not one postfix chain (an operator, a comma, whitespace or an
39
+ * unbalanced bracket at depth 0).
40
+ *
41
+ * The result names the last member, whether the chain ends in a call of it,
42
+ * and `head`, the chain text before that call (or before `.member`). A JS
43
+ * regex literal may open the chain (`/^\d+$/.test(x)`).
44
+ */
45
+ function lastChainSegment(expr) {
46
+ let depth = 0;
47
+ let lastDot = -1;
48
+ let lastGroupOpen = -1;
49
+ let endsWithGroup = false;
50
+ let start = 0;
51
+ if (expr.startsWith('/')) {
52
+ let inClass = false;
53
+ let j = 1;
54
+ for (; j < expr.length; j++) {
55
+ const ch = expr[j];
56
+ if (ch === '\\') {
57
+ j++;
58
+ continue;
59
+ }
60
+ if (ch === '[')
61
+ inClass = true;
62
+ else if (ch === ']')
63
+ inClass = false;
64
+ else if (ch === '/' && !inClass)
65
+ break;
66
+ }
67
+ if (j >= expr.length)
68
+ return null;
69
+ start = j + 1;
70
+ }
71
+ for (let i = start; i < expr.length; i++) {
72
+ const c = expr[i];
73
+ if (c === '"' || c === "'" || c === '`') {
74
+ if (c === '`' && depth === 0)
75
+ return null; // template text may interpolate
76
+ let j = i + 1;
77
+ for (; j < expr.length && expr[j] !== c; j++)
78
+ if (expr[j] === '\\')
79
+ j++;
80
+ if (j >= expr.length)
81
+ return null;
82
+ i = j;
83
+ endsWithGroup = false;
84
+ continue;
85
+ }
86
+ if (c === '(' || c === '[' || c === '{') {
87
+ if (depth === 0)
88
+ lastGroupOpen = i;
89
+ depth++;
90
+ continue;
91
+ }
92
+ if (c === ')' || c === ']' || c === '}') {
93
+ depth--;
94
+ if (depth < 0)
95
+ return null;
96
+ if (depth === 0)
97
+ endsWithGroup = true;
98
+ continue;
99
+ }
100
+ if (depth > 0)
101
+ continue;
102
+ if (c === '.') {
103
+ // `?.` optional chaining is still one chain
104
+ lastDot = i;
105
+ endsWithGroup = false;
106
+ continue;
107
+ }
108
+ if (c === '?' && expr[i + 1] === '.')
109
+ continue;
110
+ if (/[\w$]/.test(c)) {
111
+ endsWithGroup = false;
112
+ continue;
113
+ }
114
+ return null;
115
+ }
116
+ if (depth !== 0)
117
+ return null;
118
+ if (endsWithGroup) {
119
+ // Trailing `( … )` must be the call of the last member.
120
+ if (expr[lastGroupOpen] !== '(')
121
+ return null;
122
+ const callee = expr.slice(0, lastGroupOpen).replace(/\?$/, '');
123
+ const m = /(?:^|\.)([\w$]+)$/.exec(callee);
124
+ if (!m || lastDot > lastGroupOpen)
125
+ return null;
126
+ return { member: m[1], isCall: true, head: callee };
127
+ }
128
+ const m = /\.([\w$]+)$/.exec(expr);
129
+ if (!m)
130
+ return null;
131
+ return { member: m[1], isCall: false, head: expr.slice(0, expr.length - m[0].length) };
132
+ }
133
+ /** True when `rhs` is a whole-expression measure or test of a value. */
134
+ export function isScalarDerivation(rhs, language) {
135
+ const lang = scalarLanguage(language);
136
+ if (!lang)
137
+ return false;
138
+ let e = rhs.trim();
139
+ // A leading negation keeps a boolean a boolean.
140
+ while (e.startsWith('!'))
141
+ e = e.slice(1).trim();
142
+ if (e.length === 0)
143
+ return false;
144
+ const seg = lastChainSegment(e);
145
+ if (!seg)
146
+ return false;
147
+ if (lang === 'go') {
148
+ if (!seg.isCall)
149
+ return false;
150
+ if (GO_SCALAR_FUNCS.has(seg.head))
151
+ return true;
152
+ const pkg = /^(strings|bytes)\.(\w+)$/.exec(seg.head);
153
+ if (pkg)
154
+ return GO_STRINGS_SCALAR.has(pkg[2]);
155
+ return seg.head.includes('.') && GO_REGEXP_BOOL.has(seg.member);
156
+ }
157
+ if (!seg.isCall)
158
+ return seg.member === 'length';
159
+ return seg.head.includes('.') && JS_SCALAR_METHODS.has(seg.member);
160
+ }
161
+ /**
162
+ * The initializer of a single-variable local declared or assigned on `line`,
163
+ * or null when the line is not a complete single-target assignment of `name`.
164
+ */
165
+ function initializerOf(line, name, lang) {
166
+ const esc = name.replace(/[$]/g, '\\$');
167
+ const code = line.replace(/\s*\/\/.*$/, '').trim().replace(/;$/, '').trim();
168
+ const re = lang === 'go'
169
+ ? new RegExp(`^(?:var\\s+)?${esc}(?:\\s+\\w+)?\\s*(?::=|=)\\s*(.+)$`)
170
+ : new RegExp(`^(?:(?:const|let|var)\\s+)?${esc}(?:\\s*:\\s*(?:number|boolean))?\\s*=(?!=)\\s*(.+)$`);
171
+ const m = re.exec(code);
172
+ return m ? m[1] : null;
173
+ }
174
+ /**
175
+ * DFG def ids whose value is an integer or boolean derived from another value
176
+ * by a whole-expression measure or test. Taint does not flow into these defs.
177
+ */
178
+ export function scalarDerivedDefIds(defs, code, language) {
179
+ const out = new Set();
180
+ const lang = scalarLanguage(language);
181
+ if (!lang)
182
+ return out;
183
+ const lines = code.split('\n');
184
+ for (const def of defs) {
185
+ if (def.kind !== 'local')
186
+ continue;
187
+ const text = lines[def.line - 1];
188
+ if (text === undefined)
189
+ continue;
190
+ const rhs = initializerOf(text, def.variable, lang);
191
+ if (rhs !== null && isScalarDerivation(rhs, lang))
192
+ out.add(def.id);
193
+ }
194
+ return out;
195
+ }
196
+ //# sourceMappingURL=scalar-derivation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scalar-derivation.js","sourceRoot":"","sources":["../../src/analysis/scalar-derivation.ts"],"names":[],"mappings":"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"}
@@ -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;AAElK,OAAO,KAAK,EAAE,WAAW,EAAiB,WAAW,EAAoB,MAAM,oBAAoB,CAAC;AACpG,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAwF7E,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,CAWP;AA0JD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,WAAW,EAAE,EACtB,KAAK,EAAE,SAAS,EAAE,EAClB,IAAI,EAAE,MAAM,GACX,IAAI,CAYN;AAsjGD;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,WAAW,GACnB,OAAO,CAET"}
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;AAElK,OAAO,KAAK,EAAE,WAAW,EAAiB,WAAW,EAAoB,MAAM,oBAAoB,CAAC;AACpG,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAyF7E,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,CAWP;AA0JD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,WAAW,EAAE,EACtB,KAAK,EAAE,SAAS,EAAE,EAClB,IAAI,EAAE,MAAM,GACX,IAAI,CAYN;AAgoGD;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,WAAW,GACnB,OAAO,CAET"}
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { DOM_ASSIGNMENT_SINK_METHODS, isProvablyLiteralExpression } from './literal-expression.js';
7
7
  import { getDefaultConfig } from './config-loader.js';
8
+ import { javaReplaceCrLfCoverage } from './java-crlf-strip.js';
8
9
  import { resolveIdentifierType, resolveCallReturnType, isBoundedClassType, } from './arg-type-resolver.js';
9
10
  /**
10
11
  * Python tainted access patterns (regex-based).
@@ -86,7 +87,7 @@ export function analyzeTaint(calls, types, config = getDefaultConfig(), typeHier
86
87
  sinkPatterns = expandIndirectEvalAliases(sinkPatterns, sourceLines, language);
87
88
  const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines, types);
88
89
  const gatedSinks = language === 'csharp' ? dropCSharpObjectCarriedWaypoints(sinks, calls) : sinks;
89
- const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
90
+ const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines, language);
90
91
  return { sources, sinks: gatedSinks, sanitizers };
91
92
  }
92
93
  /**
@@ -735,8 +736,11 @@ function isParameterizedQueryCall(call, pattern) {
735
736
  // Arg-0 behaviour is unchanged: a placeholder literal plus any further
736
737
  // argument is parameterized. Context forms look at the query argument
737
738
  // the sink names, and require a bound argument after it.
738
- const queryPos = pattern.arg_positions.length > 0
739
- ? Math.min(...pattern.arg_positions)
739
+ // A sink loaded from an untyped rules file can omit `arg_positions`;
740
+ // treat that as empty, as the subtype-arity check does (#683).
741
+ const positions = pattern.arg_positions ?? [];
742
+ const queryPos = positions.length > 0
743
+ ? Math.min(...positions)
740
744
  : 0;
741
745
  const queryArg = call.arguments.find(a => a.position === queryPos);
742
746
  if (queryArg) {
@@ -1887,6 +1891,49 @@ function isGoFprintToNonResponseWriter(call, pattern, language, sourceLines) {
1887
1891
  }
1888
1892
  return true;
1889
1893
  }
1894
+ /**
1895
+ * JDK types whose `getOutputStream()` is an outbound channel (a remote server,
1896
+ * a peer socket, a child process) rather than the servlet response.
1897
+ */
1898
+ const JAVA_OUTBOUND_STREAM_OWNERS = 'HttpURLConnection|HttpsURLConnection|URLConnection|JarURLConnection|Socket|SSLSocket|Process';
1899
+ /**
1900
+ * #671 — true when a Java xss sink is `<base>.getOutputStream().<write>(...)`
1901
+ * and `<base>` is declared in this file as a `URLConnection` / `Socket` /
1902
+ * `Process` (or the chain is `….openConnection().getOutputStream()`). Bytes
1903
+ * written there leave for another host or process; nothing is rendered in the
1904
+ * requester's browser. Positive evidence only: an unresolved base keeps the
1905
+ * sink.
1906
+ */
1907
+ function isJavaOutboundOutputStreamWrite(call, pattern, language, sourceLines) {
1908
+ if (language !== 'java' || pattern.type !== 'xss')
1909
+ return false;
1910
+ const receiver = (call.receiver ?? '').replace(/\s+/g, '');
1911
+ if (!receiver.endsWith('.getOutputStream()'))
1912
+ return false;
1913
+ if (/\.openConnection\(\)\)?\.getOutputStream\(\)$/.test(receiver))
1914
+ return true;
1915
+ const m = receiver.match(/^\(?([A-Za-z_]\w*)\)?\.getOutputStream\(\)$/);
1916
+ if (!m || !sourceLines)
1917
+ return false;
1918
+ const declRe = new RegExp(`\\b(?:[\\w.]+\\.)?(?:${JAVA_OUTBOUND_STREAM_OWNERS})\\s+${m[1]}\\s*[=;,)]`);
1919
+ // Nearest declaration above the call wins; a re-declaration with another
1920
+ // type (shadowing) stops the walk.
1921
+ // `var x = …` (inferred type) also shadows, and its type is unknown here.
1922
+ const anyDeclRe = new RegExp(`\\b(?:[A-Z][\\w.<>,\\[\\]]*|var)\\s+${m[1]}\\s*[=;,)]`);
1923
+ for (let i = call.location.line - 1; i >= 0; i--) {
1924
+ // Strip string literals, `//` comments and `/* … */` block comments so a
1925
+ // commented-out declaration cannot match either regex.
1926
+ const line = (sourceLines[i] ?? '')
1927
+ .replace(/"(?:[^"\\]|\\.)*"/g, '""')
1928
+ .replace(/\/\*[\s\S]*?\*\//g, '')
1929
+ .replace(/\/\/.*$/, '');
1930
+ if (declRe.test(line))
1931
+ return true;
1932
+ if (anyDeclRe.test(line))
1933
+ return false;
1934
+ }
1935
+ return false;
1936
+ }
1890
1937
  /**
1891
1938
  * Gate for Go `json.Unmarshal(data, &dst)` / `json.NewDecoder(r).Decode(&dst)`
1892
1939
  * deserialization (CWE-502) sinks. Returns `true` when the destination is a
@@ -2268,6 +2315,12 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
2268
2315
  if (isGoFprintToNonResponseWriter(call, pattern, language, sourceLines)) {
2269
2316
  continue;
2270
2317
  }
2318
+ // #671 — `conn.getOutputStream().write(...)` on an outbound
2319
+ // URLConnection / Socket / Process writes to a remote peer or child
2320
+ // process, not to the HTTP response. Not xss.
2321
+ if (isJavaOutboundOutputStreamWrite(call, pattern, language, sourceLines)) {
2322
+ continue;
2323
+ }
2271
2324
  // #147 — Jinja2 safe render-context. Template("literal").render(**ctx)
2272
2325
  // and render_template_string("literal", **ctx) and Template("literal")
2273
2326
  // alone are safe: the template SOURCE is a constant string and Jinja2
@@ -2803,6 +2856,21 @@ function receiverMightBeClassImpl(receiver, className) {
2803
2856
  if (receiver === className) {
2804
2857
  return true;
2805
2858
  }
2859
+ // Inline CommonJS require receiver: `require('child_process').exec(x)`.
2860
+ // The module is named at the call, so match it as if it were bound to a
2861
+ // name (`const cp = require('child_process'); cp.exec(x)`). (cognium-dev#682)
2862
+ const inlineRequire = receiver.match(/^require\(\s*['"`](?:node:)?([\w./-]+)['"`]\s*\)$/);
2863
+ if (inlineRequire && inlineRequire[1] === className) {
2864
+ return true;
2865
+ }
2866
+ // Chained JDK ClassLoader accessors: `Thread.currentThread()
2867
+ // .getContextClassLoader().loadClass(x)`, `getClass().getClassLoader()
2868
+ // .loadClass(x)`, `ClassLoader.getSystemClassLoader().loadClass(x)`.
2869
+ // These return a ClassLoader, so match the receiver as one. (cognium-dev#623)
2870
+ if (className === 'ClassLoader' &&
2871
+ /(?:^|\.)(?:getContextClassLoader|getClassLoader|getSystemClassLoader)\(\s*\)$/.test(receiver.replace(/\s+/g, ''))) {
2872
+ return true;
2873
+ }
2806
2874
  // Constructor-call receiver: `ClassName(args)` — Python `Path(raw)`,
2807
2875
  // JS `new URL(s)` (handled separately), etc. When the receiver is a
2808
2876
  // direct function call whose function name equals the target class
@@ -3192,8 +3260,11 @@ export function isInDangerousPosition(argPosition, pattern) {
3192
3260
  /**
3193
3261
  * Find sanitizers in method calls and annotated parameters.
3194
3262
  */
3195
- function findSanitizers(calls, types, patterns, sourceLines) {
3263
+ function findSanitizers(calls, types, patterns, sourceLines, language) {
3196
3264
  const sanitizers = [];
3265
+ const javaReplace = (call) => language === 'java' && sourceLines && (call.method_name === 'replace' || call.method_name === 'replaceAll')
3266
+ ? javaReplaceChainCredit(javaChainTextAt(sourceLines, call.location.line, call.location.column))
3267
+ : null;
3197
3268
  // Build a set of method names with @sanitizer annotation (from Javadoc comments)
3198
3269
  const sanitizerMethods = new Set();
3199
3270
  for (const type of types) {
@@ -3237,10 +3308,16 @@ function findSanitizers(calls, types, patterns, sourceLines) {
3237
3308
  continue;
3238
3309
  const innerCall = inside[0];
3239
3310
  let matched;
3240
- for (const pattern of patterns) {
3241
- if (matchesSanitizerPattern(innerCall, pattern)) {
3242
- matched = pattern;
3243
- break;
3311
+ const replaceCredit = javaReplace(innerCall);
3312
+ if (replaceCredit) {
3313
+ matched = replaceCredit.length > 0 ? { method: innerCall.method_name, removes: replaceCredit } : undefined;
3314
+ }
3315
+ else {
3316
+ for (const pattern of patterns) {
3317
+ if (matchesSanitizerPattern(innerCall, pattern)) {
3318
+ matched = pattern;
3319
+ break;
3320
+ }
3244
3321
  }
3245
3322
  }
3246
3323
  if (!matched || !matched.removes || matched.removes.length === 0)
@@ -3303,6 +3380,18 @@ function findSanitizers(calls, types, patterns, sourceLines) {
3303
3380
  });
3304
3381
  continue; // Skip pattern matching - already added as sanitizer
3305
3382
  }
3383
+ const replaceCredit = javaReplace(call);
3384
+ if (replaceCredit) {
3385
+ if (replaceCredit.length > 0) {
3386
+ sanitizers.push({
3387
+ type: 'method_call',
3388
+ method: formatSanitizerMethod(call),
3389
+ line: call.location.line,
3390
+ sanitizes: replaceCredit,
3391
+ });
3392
+ }
3393
+ continue;
3394
+ }
3306
3395
  for (const pattern of patterns) {
3307
3396
  if (matchesSanitizerPattern(call, pattern)) {
3308
3397
  sanitizers.push({
@@ -3333,6 +3422,81 @@ function findSanitizers(calls, types, patterns, sourceLines) {
3333
3422
  }
3334
3423
  return sanitizers;
3335
3424
  }
3425
+ const JAVA_LIT = String.raw `(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)')`;
3426
+ const JAVA_REPLACE_ARGS_RE = new RegExp(String.raw `\.(replace|replaceAll)\(\s*${JAVA_LIT}\s*,\s*${JAVA_LIT}\s*\)`, 'g');
3427
+ /**
3428
+ * The text of the call chain that starts at (line, column): the receiver
3429
+ * expression plus the `.method(...)` segments after it, up to the first
3430
+ * character that cannot continue the chain at nesting depth 0 (`;`, `,`, an
3431
+ * operator, an unmatched closing bracket). A call reports the start of the
3432
+ * whole chain it ends, so `a.replace("\n", "")` and `b.replace("\r", "")` on one
3433
+ * line start at different columns and never credit each other. The chain may
3434
+ * continue over following lines.
3435
+ */
3436
+ function javaChainTextAt(sourceLines, line, column) {
3437
+ const text = [(sourceLines[line - 1] ?? '').slice(column), ...sourceLines.slice(line, line + 7)].join('\n');
3438
+ let depth = 0;
3439
+ let i = 0;
3440
+ for (; i < text.length; i++) {
3441
+ const c = text[i];
3442
+ if (c === '"' || c === "'") {
3443
+ for (i++; i < text.length && text[i] !== c; i++)
3444
+ if (text[i] === '\\')
3445
+ i++;
3446
+ continue;
3447
+ }
3448
+ if (c === '(' || c === '[') {
3449
+ depth++;
3450
+ continue;
3451
+ }
3452
+ if (c === ')' || c === ']') {
3453
+ if (depth === 0)
3454
+ break;
3455
+ depth--;
3456
+ continue;
3457
+ }
3458
+ if (depth === 0 && !/[\w$.\s]/.test(c))
3459
+ break;
3460
+ }
3461
+ return text.slice(0, i);
3462
+ }
3463
+ /**
3464
+ * Java `String.replace` / `replaceAll` credit, decided from the literal
3465
+ * arguments of the replace chain that contains the call instead of the method name
3466
+ * (cognium-dev#562 / #605 / #517). The class-less `replace` row credited any
3467
+ * replace as a log-injection sanitizer, so an LF-only strip silenced the
3468
+ * finding while `replaceAll("[\r\n]", "")` got no credit at all.
3469
+ * - `log_injection`: the chain strips both CR and LF (double-quoted String or single-quoted char literals).
3470
+ * - `xss`: the chain maps both `<` and `>` to an entity.
3471
+ * Returns the credited sink types (possibly empty).
3472
+ */
3473
+ function javaReplaceChainCredit(chainText) {
3474
+ let cr = false;
3475
+ let lf = false;
3476
+ let lt = false;
3477
+ let gt = false;
3478
+ for (const m of chainText.matchAll(JAVA_REPLACE_ARGS_RE)) {
3479
+ const kind = m[1];
3480
+ const rawFrom = m[2] ?? m[3] ?? '';
3481
+ const from = rawFrom.replace(/\\\\/g, '\\');
3482
+ const to = m[4] ?? m[5] ?? '';
3483
+ const cov = javaReplaceCrLfCoverage(kind, rawFrom);
3484
+ if (cov.cr)
3485
+ cr = true;
3486
+ if (cov.lf)
3487
+ lf = true;
3488
+ if (from === '<' && to.startsWith('&'))
3489
+ lt = true;
3490
+ if (from === '>' && to.startsWith('&'))
3491
+ gt = true;
3492
+ }
3493
+ const out = [];
3494
+ if (cr && lf)
3495
+ out.push('log_injection');
3496
+ if (lt && gt)
3497
+ out.push('xss');
3498
+ return out;
3499
+ }
3336
3500
  /**
3337
3501
  * Check if a call matches a sanitizer pattern.
3338
3502
  */