@clear-capabilities/agentic-security-scanner 0.151.3 → 0.152.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/CHANGELOG.md +71 -0
- package/dist/agentic-security.mjs +80 -80
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +5 -5
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +802 -13
- package/src/dataflow/engine.js +852 -89
- package/src/dataflow/incremental.js +13 -0
- package/src/dataflow/index.js +18 -5
- package/src/dataflow/path-feasibility.js +86 -9
- package/src/dataflow/string-domain.js +132 -0
- package/src/dataflow/summaries.js +69 -8
- package/src/engine.js +108 -3
- package/src/ir/CLAUDE.md +2 -1
- package/src/ir/balanced-call.js +33 -1
- package/src/ir/callgraph.js +26 -2
- package/src/ir/class-hierarchy.js +101 -6
- package/src/ir/csharp-ir.js +97 -8
- package/src/ir/index.js +21 -1
- package/src/ir/parser-cs.js +706 -24
- package/src/ir/parser-java.js +498 -17
- package/src/ir/parser-js.js +22 -0
- package/src/ir/parser-php.js +446 -43
- package/src/ir/parser-rust.js +1338 -0
- package/src/ir/php-include-merge.js +190 -0
- package/src/lineage/sink-registry.js +18 -0
- package/src/lineage/source-registry.js +23 -0
- package/src/posture/csharp-analysis.js +341 -43
- package/src/posture/finding-defaults.js +5 -1
- package/src/posture/fix-coverage.js +1 -0
- package/src/posture/proof-coverage.js +1 -0
- package/src/sast/CLAUDE.md +1 -1
- package/src/sast/authz.js +144 -1
- package/src/sast/crypto-protocol.js +65 -7
- package/src/sast/csharp-cross-file.js +125 -0
- package/src/sast/csharp-structural.js +78 -2
- package/src/sast/csharp.js +574 -42
- package/src/sast/java-ast-folding.js +268 -11
- package/src/sast/java-bench-extras.js +530 -2
- package/src/sast/java-deserialization.js +31 -2
- package/src/sast/java-structural-cross-file.js +146 -0
- package/src/sast/java-structural.js +499 -6
- package/src/sast/ldap-injection.js +133 -3
- package/src/sast/rust.js +57 -0
- package/src/sast/xss-reflected-multilang.js +252 -3
|
@@ -54,7 +54,9 @@ const SETX_RE = /\.\s*set(?:String|Int|Long|Object|Date|Timestamp|Boolean|Float|
|
|
|
54
54
|
// ─── New-rule patterns ────────────────────────────────────────────────────
|
|
55
55
|
|
|
56
56
|
// CWE-601: response.sendRedirect(<tainted-or-non-literal>)
|
|
57
|
-
|
|
57
|
+
// Exported for java-structural-cross-file.js (W5.42) — the same sink
|
|
58
|
+
// matching, reused to re-derive candidate findings for cross-file checking.
|
|
59
|
+
export const SEND_REDIRECT_RE = /\b(?:response|resp|res)\s*\.\s*sendRedirect\s*\(\s*([^)]+)\)/g;
|
|
58
60
|
|
|
59
61
|
// CWE-319: cleartext transmission of sensitive information.
|
|
60
62
|
//
|
|
@@ -75,6 +77,18 @@ const SEND_REDIRECT_RE = /\b(?:response|resp|res)\s*\.\s*sendRedirect\s*\(\s*([^
|
|
|
75
77
|
const INSECURE_URL_LITERAL_RE = /\bnew\s+URL\s*\(\s*"http:\/\/[^"]*"\s*\)/g;
|
|
76
78
|
const INSECURE_URL_CONCAT_RE = /\bnew\s+URL\s*\(\s*"http:\/\/[^"]*"\s*\+\s*\w/g;
|
|
77
79
|
const RAW_SOCKET_RE = /\bnew\s+Socket\s*\(\s*[^)]+\)/g;
|
|
80
|
+
// SARD_80_F1 W4.J29 — `RAW_SOCKET_RE` only matches the CLIENT side
|
|
81
|
+
// (`new Socket(host, port)`); Juliet's `listen_tcp_*` descriptor family
|
|
82
|
+
// (confirmed against the public mirror,
|
|
83
|
+
// `CWE319_Cleartext_Tx_Sensitive_Info__listen_tcp_driverManager_01.java`)
|
|
84
|
+
// is the SERVER side instead — `ServerSocket listener = new
|
|
85
|
+
// ServerSocket(port); Socket socket = listener.accept();` — which never
|
|
86
|
+
// constructs a `Socket` directly at all, so it was a total blackout for
|
|
87
|
+
// this whole descriptor family (82 of 176 CWE-319 test-split entries,
|
|
88
|
+
// confirmed by grouping the false negatives by descriptor base name).
|
|
89
|
+
// `.accept()` always returns a `Socket`, the identical "got a raw,
|
|
90
|
+
// unencrypted socket" moment `new Socket(...)` already fires on.
|
|
91
|
+
const SERVERSOCKET_ACCEPT_RE = /\.accept\s*\(\s*\)/g;
|
|
78
92
|
|
|
79
93
|
// "Sensitive-data context" — file contains any of these identifiers.
|
|
80
94
|
// Variable names like `password`, `passwd`, `secret`, `token`, `cred`, etc.
|
|
@@ -93,6 +107,467 @@ const SENSITIVE_COOKIE_RE = /\bnew\s+Cookie\s*\(\s*"(?:session|sess|token|auth|j
|
|
|
93
107
|
// Match `new Cookie(literal, NON_LITERAL_VAR)` regardless of cookie name.
|
|
94
108
|
const RESPONSE_SPLITTING_COOKIE_RE = /\bnew\s+Cookie\s*\(\s*"[^"]*"\s*,\s*([A-Za-z_]\w*)\s*\)/g;
|
|
95
109
|
|
|
110
|
+
// CWE-259: hard-coded password reaching a credential parameter. Juliet's
|
|
111
|
+
// canonical shape (confirmed via the public mirror, all three sink variants):
|
|
112
|
+
// a String variable is set to a HARDCODED LITERAL, then used — by NAME, not
|
|
113
|
+
// inline — as the password/credential argument of one of these APIs. This is
|
|
114
|
+
// the inverse of ordinary taint detection: the finding fires when the value
|
|
115
|
+
// is PROVABLY a constant, not when it's tainted, so the taint engine's own
|
|
116
|
+
// sink-matching (which fires on TAINTED args) structurally cannot express it.
|
|
117
|
+
//
|
|
118
|
+
// DriverManager.getConnection(url, user, data) — data used bare
|
|
119
|
+
// new KerberosKey(principal, data.toCharArray(), ...) — data.toCharArray()
|
|
120
|
+
// new PasswordAuthentication(user, data.toCharArray())— data.toCharArray()
|
|
121
|
+
//
|
|
122
|
+
// Each regex captures the credential identifier (stripping a trailing
|
|
123
|
+
// `.toCharArray()` where present).
|
|
124
|
+
const HARDCODED_PW_DRIVERMANAGER_RE = /\bDriverManager\s*\.\s*getConnection\s*\([^,]+,[^,]+,\s*([A-Za-z_]\w*)\s*\)/g;
|
|
125
|
+
const HARDCODED_PW_KERBEROSKEY_RE = /\bnew\s+KerberosKey\s*\([^,]+,\s*([A-Za-z_]\w*)\s*\.\s*toCharArray\s*\(\s*\)/g;
|
|
126
|
+
const HARDCODED_PW_PASSWORDAUTH_RE = /\bnew\s+PasswordAuthentication\s*\([^,]+,\s*([A-Za-z_]\w*)\s*\.\s*toCharArray\s*\(\s*\)/g;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* True when the NEAREST assignment to `varName` before `beforeIdx` (source
|
|
130
|
+
* order, not lexical scope — see the header comment above `scanJavaBenchExtras`'s
|
|
131
|
+
* CWE-259 block for why a backward nearest-assignment scan is enough to stay
|
|
132
|
+
* correctly scoped to the enclosing method without a full method-boundary
|
|
133
|
+
* parse) is a string-literal assignment, not a call/variable/concat RHS.
|
|
134
|
+
* Deliberately conservative: a variable never assigned at all (`beforeIdx`
|
|
135
|
+
* before any assignment) returns false, same direction as every other
|
|
136
|
+
* evidence-required check in this codebase.
|
|
137
|
+
*/
|
|
138
|
+
// SARD_80_F1 W4.J33 — Juliet's own "if(true){…} else{ x = null; }" / "if
|
|
139
|
+
// (false){ x = null; } else {…}" dead-code idiom (its own in-source comment:
|
|
140
|
+
// "INCIDENTAL: CWE 561 Dead Code, the code below will never run but ensure
|
|
141
|
+
// [x] is initialized before the Sink to avoid compiler errors") is used
|
|
142
|
+
// across dozens of CWEs and flow variants (confirmed via the public mirror
|
|
143
|
+
// — the SAME idiom this session already fixed for C#'s CFG at W4.C28,
|
|
144
|
+
// manifesting here as a text-scan bug instead of a CFG bug). This backward,
|
|
145
|
+
// text-order-only scan for "the nearest assignment before the sink" has no
|
|
146
|
+
// notion of dead code: `if (true) { data = "foo"; } else { data = null; }`
|
|
147
|
+
// puts the LITERAL assignment first and the DEAD `data = null;` second —
|
|
148
|
+
// textually LATER, so `lastAnyEnd` (computed from ANY assignment) lands on
|
|
149
|
+
// the dead one, `lastLiteralEnd !== lastAnyEnd` fails, and a genuinely safe
|
|
150
|
+
// goodG2B() variant using this exact idiom was never recognized as
|
|
151
|
+
// literal-only, leaving a real false positive uncorrected (confirmed via
|
|
152
|
+
// the public mirror's own `CWE601_Open_Redirect__Servlet_connect_tcp_02
|
|
153
|
+
// .java`'s `goodG2B2()`). Fixed by reusing the ALREADY-EXISTING, real
|
|
154
|
+
// AST-based `deadBranchRanges`/`isLineInDeadRange` (java-ast-folding.js,
|
|
155
|
+
// already used by `applyJavaBenchSuppressions` for a different purpose) to
|
|
156
|
+
// skip any assignment whose line falls inside a provably-dead branch when
|
|
157
|
+
// computing `lastAnyEnd` — the literal assignment inside the LIVE branch is
|
|
158
|
+
// then correctly recognized as the value that actually reaches the sink.
|
|
159
|
+
// `deadRanges` is optional (empty array when the caller has none, or on a
|
|
160
|
+
// parse failure) so every existing call site keeps its old, more
|
|
161
|
+
// conservative behavior unless it opts in.
|
|
162
|
+
// SARD_80_F1 W4.J37 — Juliet's "data returned from one method to another in
|
|
163
|
+
// the same class" flow variants (its own template naming: sources-sink-41+,
|
|
164
|
+
// the exact sibling idiom to the argument-passing one W4.J33's own header
|
|
165
|
+
// comment already documents) route a PROVABLY-literal value through a
|
|
166
|
+
// same-file helper's RETURN value instead of a direct assignment:
|
|
167
|
+
// `private String goodG2BSource() { return "foo"; } ... String data =
|
|
168
|
+
// goodG2BSource(); response.sendRedirect(data);` — confirmed against the
|
|
169
|
+
// public mirror's own `CWE601_Open_Redirect__Servlet_connect_tcp_42.java`.
|
|
170
|
+
// `_nearestAssignIsLiteral`'s own backward scan sees `data = goodG2BSource()`
|
|
171
|
+
// as a NON-literal "any assignment" (the RHS is a call, not a quoted
|
|
172
|
+
// string), so `lastLiteralEnd !== lastAnyEnd` correctly-by-its-own-rules
|
|
173
|
+
// fails closed — except the callee ALWAYS returns a literal, so the value
|
|
174
|
+
// genuinely is safe. Deliberately narrow and bounded, matching this file's
|
|
175
|
+
// existing conservative-evidence-required convention: only a callee with
|
|
176
|
+
// EXACTLY ONE `return` statement in its own body is resolved (multiple
|
|
177
|
+
// returns are ambiguous — fails closed, never guessed); the returned
|
|
178
|
+
// expression must itself be a literal OR a bare identifier that recurses
|
|
179
|
+
// through this SAME resolution (bounded by `_depth`, matching the codebase's
|
|
180
|
+
// established recursion-guard convention elsewhere) — never a call, concat,
|
|
181
|
+
// or ternary, which all fail closed. Same-file only: `content` is always
|
|
182
|
+
// this one file's own text, so there is no cross-file resolution risk here.
|
|
183
|
+
const _CALLEE_RETURN_LITERAL_CACHE_DEPTH = 4;
|
|
184
|
+
function _resolveCalleeReturnIsLiteral(content, calleeName, deadRanges, _depth) {
|
|
185
|
+
const depth = _depth || 0;
|
|
186
|
+
if (depth >= _CALLEE_RETURN_LITERAL_CACHE_DEPTH) return false;
|
|
187
|
+
const escapedCallee = calleeName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
188
|
+
// W4.J39 — a confirmed, severe ReDoS: the ORIGINAL pattern here matched a
|
|
189
|
+
// "return type" prefix via a LAZY character class that itself included
|
|
190
|
+
// `\s` (`[\w.<>\[\],\s]+?`). Comment-blanked content (`scanJavaBenchExtras`
|
|
191
|
+
// always calls `_resolveCalleeReturnIsLiteral` — and everything downstream
|
|
192
|
+
// of it — with `blankComments(raw)`, never `raw`) turns Juliet's own
|
|
193
|
+
// large header comment blocks into a LONG run of blanked whitespace, and
|
|
194
|
+
// a lazy quantifier whose OWN character class includes `\s` backtracks
|
|
195
|
+
// catastrophically trying every possible split point inside that run —
|
|
196
|
+
// confirmed via direct reproduction (hangs indefinitely on a real, small
|
|
197
|
+
// 8KB corpus file; the exact same shape hung a live full-corpus scan for
|
|
198
|
+
// 30+ minutes on a single small CWE before it was caught and killed).
|
|
199
|
+
// Fixed by dropping the "return type" prefix match ENTIRELY: since the
|
|
200
|
+
// callee NAME is already known, a bare `\bcalleeName\s*(...) {` search
|
|
201
|
+
// is exactly as identifying (a call site is never immediately followed
|
|
202
|
+
// by `{`) without ever touching a `\s`-inclusive lazy quantifier.
|
|
203
|
+
const declRe = new RegExp(`\\b${escapedCallee}\\s*\\([^)]*\\)\\s*(?:throws\\s+[\\w.,\\s]+)?\\s*\\{`, 'g');
|
|
204
|
+
const dm = declRe.exec(content);
|
|
205
|
+
if (!dm) return false;
|
|
206
|
+
const bodyStart = dm.index + dm[0].length;
|
|
207
|
+
let braceDepth = 1, i = bodyStart;
|
|
208
|
+
while (i < content.length && braceDepth > 0) {
|
|
209
|
+
if (content[i] === '{') braceDepth++;
|
|
210
|
+
else if (content[i] === '}') braceDepth--;
|
|
211
|
+
i++;
|
|
212
|
+
}
|
|
213
|
+
const body = content.slice(bodyStart, i - 1);
|
|
214
|
+
const returnRe = /\breturn\s+([^;]+);/g;
|
|
215
|
+
const returns = [];
|
|
216
|
+
let rm;
|
|
217
|
+
while ((rm = returnRe.exec(body))) returns.push({ expr: rm[1].trim(), idx: bodyStart + rm.index });
|
|
218
|
+
if (returns.length !== 1) return false; // ambiguous (0 or 2+ returns) — fail closed
|
|
219
|
+
const { expr, idx } = returns[0];
|
|
220
|
+
if (/^"[^"]*"$/.test(expr)) return true;
|
|
221
|
+
if (/^[A-Za-z_]\w*$/.test(expr)) {
|
|
222
|
+
return _nearestAssignIsLiteral(content, expr, idx, deadRanges, depth + 1);
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Balanced top-level comma split (paren/bracket/brace-aware, quote-aware —
|
|
228
|
+
// an argument like `"a, b"` or `foo(1, 2)` must not be split on its OWN
|
|
229
|
+
// internal commas). Deliberately simple: no need to track Java generics'
|
|
230
|
+
// `<...>` nesting specifically, since angle brackets never appear in a
|
|
231
|
+
// plain call argument list the way they do in a type declaration.
|
|
232
|
+
function _splitTopLevelCommas(text) {
|
|
233
|
+
const parts = [];
|
|
234
|
+
let depth = 0, cur = '', inStr = null;
|
|
235
|
+
for (let i = 0; i < text.length; i++) {
|
|
236
|
+
const c = text[i];
|
|
237
|
+
if (inStr) {
|
|
238
|
+
cur += c;
|
|
239
|
+
if (c === '\\') { cur += text[++i] || ''; continue; }
|
|
240
|
+
if (c === inStr) inStr = null;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (c === '"' || c === "'") { inStr = c; cur += c; continue; }
|
|
244
|
+
if (c === '(' || c === '[' || c === '{') depth++;
|
|
245
|
+
else if (c === ')' || c === ']' || c === '}') depth--;
|
|
246
|
+
if (c === ',' && depth === 0) { parts.push(cur); cur = ''; continue; }
|
|
247
|
+
cur += c;
|
|
248
|
+
}
|
|
249
|
+
if (cur.trim() !== '' || parts.length) parts.push(cur);
|
|
250
|
+
return parts;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// SARD_80_F1 W4.J38 — the sibling gap to W4.J33's own parameter-boundary
|
|
254
|
+
// guard (see that guard's own header comment on the "data passed as an
|
|
255
|
+
// argument from one method to another" idiom). That guard assumes the
|
|
256
|
+
// CALLER's own literal assignment appears textually BEFORE the callee
|
|
257
|
+
// method in the file — the common case — but Juliet's own file layout does
|
|
258
|
+
// NOT guarantee this: confirmed via the public mirror's own
|
|
259
|
+
// `CWE601_Open_Redirect__Servlet_connect_tcp_41.java`, where `goodG2BSink`
|
|
260
|
+
// (the method actually containing the sink call) is DEFINED BEFORE its own
|
|
261
|
+
// caller `goodG2B` (which supplies the literal argument) — so `goodG2B`'s
|
|
262
|
+
// `data = "foo";` sits AFTER `goodG2BSink`'s own sink call in raw file
|
|
263
|
+
// text, structurally invisible to ANY backward-only scan, however it's
|
|
264
|
+
// tuned. Rather than trying to special-case "look forward too" inside the
|
|
265
|
+
// backward scan above, this takes a completely different, WHOLE-FILE
|
|
266
|
+
// approach once the backward scan has already failed: find the METHOD that
|
|
267
|
+
// encloses `beforeIdx`, determine `varName`'s own PARAMETER POSITION in
|
|
268
|
+
// that method's signature, then check every real call site of that method
|
|
269
|
+
// (matched by bare name — Juliet's own same-class private-helper
|
|
270
|
+
// convention, mirroring `_resolveCalleeReturnIsLiteral`'s identical same-
|
|
271
|
+
// file-only scope) for a literal at the SAME argument position. Fails
|
|
272
|
+
// closed at every step: zero real call sites found, an arity mismatch, or
|
|
273
|
+
// ANY call site supplying a non-literal at that position all return
|
|
274
|
+
// `false` — this only ever ADDS a positive signal when literally every
|
|
275
|
+
// known caller agrees, never guesses from a single ambiguous data point.
|
|
276
|
+
// The declared class name of `content` itself — see java-structural.js's
|
|
277
|
+
// own identical copy of this helper (W5.41/W5.42) for the full rationale.
|
|
278
|
+
function _ownClassName(content) {
|
|
279
|
+
const m = /\bclass\s+([A-Za-z_]\w*)/.exec(content);
|
|
280
|
+
return m ? m[1] : null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// See java-structural.js's own identical copy of this helper (W5.45) for
|
|
284
|
+
// the full rationale.
|
|
285
|
+
function _enclosingMethodSpan(text, idx) {
|
|
286
|
+
const methodDeclRe = /\b(?!if\b|for\b|while\b|switch\b|catch\b|synchronized\b|do\b|else\b|return\b|new\b)([A-Za-z_]\w*)\s*\([^)]*\)\s*(?:throws\s+[\w.,\s]+)?\s*\{/g;
|
|
287
|
+
let best = null, m;
|
|
288
|
+
while ((m = methodDeclRe.exec(text)) && m.index < idx) best = m;
|
|
289
|
+
if (!best) return null;
|
|
290
|
+
const openIdx = best.index + best[0].length - 1;
|
|
291
|
+
let depth = 1, i = openIdx + 1;
|
|
292
|
+
while (i < text.length && depth > 0) {
|
|
293
|
+
if (text[i] === '{') depth++;
|
|
294
|
+
else if (text[i] === '}') depth--;
|
|
295
|
+
i++;
|
|
296
|
+
}
|
|
297
|
+
if (i <= idx) return null;
|
|
298
|
+
return { start: openIdx, end: i };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// SARD_80_F1 W5.42 — the SAME cross-file gap W5.41 fixed in java-structural.js,
|
|
302
|
+
// ported here: Juliet's Flow Variant 51+ ("data passed as an argument from
|
|
303
|
+
// one method to another in a DIFFERENT class") splits caller/callee across
|
|
304
|
+
// two physical files in the same directory, invisible to a same-file-only
|
|
305
|
+
// search. `siblingFiles` (optional; `{code, deadRanges}` for other `.java`
|
|
306
|
+
// files in the same directory) is a fallback consulted ONLY when the
|
|
307
|
+
// same-file search finds zero real call sites — `scanJavaBenchExtras`'s own
|
|
308
|
+
// ordinary call never supplies it, so its behavior is unchanged. The
|
|
309
|
+
// cross-file fallback ALSO requires the call site to construct THIS sink's
|
|
310
|
+
// own declared class (`new ClassName()).method(`), not just call a
|
|
311
|
+
// same-named method — see java-structural.js's own W5.41 header comment for
|
|
312
|
+
// why a bare method-name match is unsound here (Juliet reuses the same
|
|
313
|
+
// generic method names identically across thousands of unrelated files in
|
|
314
|
+
// the same directory, a collision blind scrambling preserves by design).
|
|
315
|
+
function _resolveParamLiteralViaAllCallSites(content, varName, beforeIdx, deadRanges, _depth, siblingFiles) {
|
|
316
|
+
const depth0 = _depth || 0;
|
|
317
|
+
if (depth0 >= _CALLEE_RETURN_LITERAL_CACHE_DEPTH) return false;
|
|
318
|
+
const escapedVar = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
319
|
+
// Excludes Java's control-flow keywords, which have the IDENTICAL
|
|
320
|
+
// `keyword (...) {` textual shape as a method declaration (`if (x) {`,
|
|
321
|
+
// `for (...) {`, `while (...) {`, `switch (...) {`, `catch (...) {`,
|
|
322
|
+
// `synchronized (...) {`) and would otherwise be picked up as a fake
|
|
323
|
+
// "enclosing method" — confirmed the hard way: a bare `if (data != null)
|
|
324
|
+
// {` immediately inside a real sink method matched this regex FIRST,
|
|
325
|
+
// silently replacing the genuine enclosing method as `enclosing`.
|
|
326
|
+
//
|
|
327
|
+
// W4.J39 — a confirmed, severe ReDoS, the SAME defect class as
|
|
328
|
+
// `_resolveCalleeReturnIsLiteral`'s own `declRe` (see that function's own
|
|
329
|
+
// fix comment for the full reproduction): the ORIGINAL pattern here ALSO
|
|
330
|
+
// matched a "return type" prefix via a LAZY, `\s`-inclusive character
|
|
331
|
+
// class (`[\w.<>[\],\s]+?`), which backtracks catastrophically over the
|
|
332
|
+
// long blanked-whitespace runs `blankComments()` leaves where Juliet's
|
|
333
|
+
// own large header comments used to be. Fixed the same way: the "return
|
|
334
|
+
// type" prefix is never actually READ by this function (only the
|
|
335
|
+
// captured NAME and PARAMS matter), so it's dropped entirely — a bare
|
|
336
|
+
// `\b(?!keyword)IDENT\s*(...) {` search identifies a method declaration
|
|
337
|
+
// exactly as well, without ever touching a `\s`-inclusive lazy quantifier.
|
|
338
|
+
const methodDeclRe = /\b(?!if\b|for\b|while\b|switch\b|catch\b|synchronized\b|do\b|else\b|return\b|new\b)([A-Za-z_]\w*)\s*\(([^)]*)\)\s*(?:throws\s+[\w.,\s]+)?\s*\{/g;
|
|
339
|
+
let enclosing = null, dm;
|
|
340
|
+
while ((dm = methodDeclRe.exec(content)) && dm.index < beforeIdx) enclosing = dm;
|
|
341
|
+
if (!enclosing) return false;
|
|
342
|
+
const methodName = enclosing[1];
|
|
343
|
+
const params = _splitTopLevelCommas(enclosing[2]).map(p => p.trim()).filter(Boolean);
|
|
344
|
+
const paramIdx = params.findIndex((p) => new RegExp(`\\b${escapedVar}$`).test(p));
|
|
345
|
+
if (paramIdx === -1) return false; // varName isn't actually a param of the enclosing method
|
|
346
|
+
const escapedMethod = methodName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
347
|
+
|
|
348
|
+
function _checkCallAt(searchText, searchDeadRanges, matchIndex, matchEnd) {
|
|
349
|
+
const argsStart = matchEnd;
|
|
350
|
+
let braceDepth = 1, i = argsStart;
|
|
351
|
+
while (i < searchText.length && braceDepth > 0) {
|
|
352
|
+
if (searchText[i] === '(') braceDepth++;
|
|
353
|
+
else if (searchText[i] === ')') braceDepth--;
|
|
354
|
+
i++;
|
|
355
|
+
}
|
|
356
|
+
const afterClose = searchText.slice(i).match(/^\s*(\{|throws)/);
|
|
357
|
+
if (afterClose) return 'skip'; // this is the method's OWN declaration, not a call site
|
|
358
|
+
const args = _splitTopLevelCommas(searchText.slice(argsStart, i - 1)).map((a) => a.trim());
|
|
359
|
+
if (paramIdx >= args.length) return 'fail'; // arity mismatch — bail conservatively
|
|
360
|
+
const argExpr = args[paramIdx];
|
|
361
|
+
if (/^"[^"]*"$/.test(argExpr)) return 'ok'; // this caller's own argument is a direct literal
|
|
362
|
+
// The caller's argument is itself a bare identifier (Juliet's own
|
|
363
|
+
// idiom: `data = "foo"; goodG2BSink(data, ...);` — the LITERAL sits on
|
|
364
|
+
// the variable one step back from the call site, not at the call site
|
|
365
|
+
// itself). Resolve it the SAME way any other candidate literal is
|
|
366
|
+
// resolved — recursing through `_nearestAssignIsLiteral` itself,
|
|
367
|
+
// scoped to the CALL SITE's own position. Bounded by the shared
|
|
368
|
+
// recursion-depth guard.
|
|
369
|
+
if (/^[A-Za-z_]\w*$/.test(argExpr) && _nearestAssignIsLiteral(searchText, argExpr, matchIndex, searchDeadRanges, depth0 + 1, siblingFiles)) return 'ok';
|
|
370
|
+
return 'fail'; // a non-literal (or unresolvable) caller exists
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function scanCallSitesIn(searchText, searchDeadRanges, classNameFilter) {
|
|
374
|
+
let sawRealCallSite = false;
|
|
375
|
+
if (classNameFilter) {
|
|
376
|
+
const inlineRe = new RegExp(`\\bnew\\s+${classNameFilter}\\s*\\(\\s*\\)\\s*\\)?\\s*\\.\\s*${escapedMethod}\\s*\\(`, 'g');
|
|
377
|
+
let cm;
|
|
378
|
+
while ((cm = inlineRe.exec(searchText))) {
|
|
379
|
+
const r = _checkCallAt(searchText, searchDeadRanges, cm.index, cm.index + cm[0].length);
|
|
380
|
+
if (r === 'skip') continue;
|
|
381
|
+
if (r === 'fail') return 'fail';
|
|
382
|
+
sawRealCallSite = true;
|
|
383
|
+
}
|
|
384
|
+
// SARD_80_F1 W5.45 — see java-structural.js's own identical fix for
|
|
385
|
+
// the full incident writeup: Juliet's abstract-dispatch caller
|
|
386
|
+
// convention is a TWO-STATEMENT pattern (`<BaseType> <var> = new
|
|
387
|
+
// <ConcreteClass>();` then later `<var>.<method>(...)`), not the
|
|
388
|
+
// inline one above. The variable's own call search is bounded to its
|
|
389
|
+
// DECLARING method's own body span (via `_enclosingMethodSpan`)
|
|
390
|
+
// since Juliet routinely reuses the same local variable name
|
|
391
|
+
// ("baseObject") across sibling bad()/goodG2B()/goodB2G() methods.
|
|
392
|
+
const declRe = new RegExp(`\\b[A-Za-z_]\\w*(?:<[^;()]*>)?\\s+([A-Za-z_]\\w*)\\s*=\\s*new\\s+${classNameFilter}\\s*\\(\\s*\\)\\s*;`, 'g');
|
|
393
|
+
let dm;
|
|
394
|
+
while ((dm = declRe.exec(searchText))) {
|
|
395
|
+
const varName2 = dm[1];
|
|
396
|
+
const span = _enclosingMethodSpan(searchText, dm.index);
|
|
397
|
+
const searchWithin = span ? searchText.slice(span.start, span.end) : searchText;
|
|
398
|
+
const offset = span ? span.start : 0;
|
|
399
|
+
const escapedVarName = varName2.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
400
|
+
const varCallRe = new RegExp(`\\b${escapedVarName}\\s*\\.\\s*${escapedMethod}\\s*\\(`, 'g');
|
|
401
|
+
let vm;
|
|
402
|
+
while ((vm = varCallRe.exec(searchWithin))) {
|
|
403
|
+
const r = _checkCallAt(searchText, searchDeadRanges, offset + vm.index, offset + vm.index + vm[0].length);
|
|
404
|
+
if (r === 'skip') continue;
|
|
405
|
+
if (r === 'fail') return 'fail';
|
|
406
|
+
sawRealCallSite = true;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
const callRe = new RegExp(`\\b${escapedMethod}\\s*\\(`, 'g');
|
|
411
|
+
let cm;
|
|
412
|
+
while ((cm = callRe.exec(searchText))) {
|
|
413
|
+
const r = _checkCallAt(searchText, searchDeadRanges, cm.index, cm.index + cm[0].length);
|
|
414
|
+
if (r === 'skip') continue;
|
|
415
|
+
if (r === 'fail') return 'fail';
|
|
416
|
+
sawRealCallSite = true;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return sawRealCallSite ? 'ok' : 'none';
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const sameFileResult = scanCallSitesIn(content, deadRanges, null);
|
|
423
|
+
if (sameFileResult === 'fail') return false;
|
|
424
|
+
let sawRealCallSite = sameFileResult === 'ok';
|
|
425
|
+
if (siblingFiles && siblingFiles.length) {
|
|
426
|
+
const ownClass = _ownClassName(content);
|
|
427
|
+
if (ownClass) {
|
|
428
|
+
const escapedClass = ownClass.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
429
|
+
for (const sib of siblingFiles) {
|
|
430
|
+
const r = scanCallSitesIn(sib.code, sib.deadRanges, escapedClass);
|
|
431
|
+
if (r === 'fail') return false;
|
|
432
|
+
if (r === 'ok') sawRealCallSite = true;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return sawRealCallSite;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// SARD_80_F1 W5.34 — see the call site's own header comment for the full
|
|
440
|
+
// idiom (Juliet Flow Variant 45: a value passed as a private class member
|
|
441
|
+
// variable between two methods of the same class). Only resolves when
|
|
442
|
+
// `varName` is confirmed to be an actual FIELD declaration — matched via an
|
|
443
|
+
// access-modifier prefix (`private`/`protected`/`public`), syntax that is
|
|
444
|
+
// valid ONLY on a class/interface member in Java, never on a local variable
|
|
445
|
+
// or a method parameter, so this can't misfire on an unrelated same-named
|
|
446
|
+
// local. Requires EVERY assignment to the field anywhere in the file
|
|
447
|
+
// (statement order doesn't matter here — unlike a backward scan, this reads
|
|
448
|
+
// the field as a whole-file invariant) to resolve to a literal, recursing
|
|
449
|
+
// through the same `_nearestAssignIsLiteral` machinery for a bare-identifier
|
|
450
|
+
// RHS (`dataGoodG2B = data;`, itself resolved at ITS OWN position so the
|
|
451
|
+
// recursive scan only sees what was visible at the time of that assignment).
|
|
452
|
+
// Fails closed: no recognizable field declaration, zero assignments found,
|
|
453
|
+
// or ANY assignment resolving to something other than a literal all return
|
|
454
|
+
// false.
|
|
455
|
+
function _resolveFieldLiteralViaAllAssignments(content, varName, deadRanges, _depth) {
|
|
456
|
+
const depth0 = _depth || 0;
|
|
457
|
+
if (depth0 >= _CALLEE_RETURN_LITERAL_CACHE_DEPTH) return false;
|
|
458
|
+
const escaped = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
459
|
+
const fieldDeclRe = new RegExp(`\\b(?:private|protected|public)\\s+[\\w<>\\[\\],.\\s]+\\b${escaped}\\s*(?:=\\s*[^;]+)?;`);
|
|
460
|
+
if (!fieldDeclRe.test(content)) return false; // not a recognizable field declaration
|
|
461
|
+
const ranges = deadRanges || [];
|
|
462
|
+
const assignRe = new RegExp(`\\b${escaped}\\s*=\\s*([^;]+);`, 'g');
|
|
463
|
+
const lineOf = (idx) => content.substring(0, idx).split('\n').length;
|
|
464
|
+
let sawAssignment = false, m;
|
|
465
|
+
while ((m = assignRe.exec(content))) {
|
|
466
|
+
if (ranges.length && isLineInDeadRange(lineOf(m.index), ranges)) continue;
|
|
467
|
+
sawAssignment = true;
|
|
468
|
+
const rhs = m[1].trim();
|
|
469
|
+
if (/^"[^"]*"$/.test(rhs)) continue; // this assignment is a direct literal
|
|
470
|
+
if (/^[A-Za-z_]\w*$/.test(rhs) && rhs !== varName
|
|
471
|
+
&& _nearestAssignIsLiteral(content, rhs, m.index, ranges, depth0 + 1)) continue;
|
|
472
|
+
return false; // a non-literal (or unresolvable) assignment exists somewhere
|
|
473
|
+
}
|
|
474
|
+
return sawAssignment;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Exported for java-structural-cross-file.js (W5.42) — see
|
|
478
|
+
// _resolveParamLiteralViaAllCallSites's own header comment for `siblingFiles`.
|
|
479
|
+
export function _nearestAssignIsLiteral(content, varName, beforeIdx, deadRanges, _calleeDepth, siblingFiles) {
|
|
480
|
+
if ((_calleeDepth || 0) >= _CALLEE_RETURN_LITERAL_CACHE_DEPTH) return false;
|
|
481
|
+
const ranges = deadRanges || [];
|
|
482
|
+
const escaped = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
483
|
+
const literalRe = new RegExp(`\\b${escaped}\\s*=\\s*"[^"]*"\\s*;`, 'g');
|
|
484
|
+
const anyAssignRe = new RegExp(`\\b${escaped}\\s*=\\s*([^;]+);`, 'g');
|
|
485
|
+
const lineOf = (idx) => content.substring(0, idx).split('\n').length;
|
|
486
|
+
let lastLiteralEnd = -1, m;
|
|
487
|
+
while ((m = literalRe.exec(content)) && m.index < beforeIdx) {
|
|
488
|
+
if (ranges.length && isLineInDeadRange(lineOf(m.index), ranges)) continue;
|
|
489
|
+
lastLiteralEnd = m.index + m[0].length;
|
|
490
|
+
}
|
|
491
|
+
let lastAnyEnd = -1, lastAnyRhs = null, lastAnyIdx = -1;
|
|
492
|
+
while ((m = anyAssignRe.exec(content)) && m.index < beforeIdx) {
|
|
493
|
+
if (ranges.length && isLineInDeadRange(lineOf(m.index), ranges)) continue;
|
|
494
|
+
lastAnyEnd = m.index + m[0].length;
|
|
495
|
+
lastAnyRhs = m[1].trim();
|
|
496
|
+
lastAnyIdx = m.index;
|
|
497
|
+
}
|
|
498
|
+
// W4.J37 — the nearest assignment's own RHS is a bare same-file call
|
|
499
|
+
// (`data = goodG2BSource();`, no receiver, no args needing evaluation):
|
|
500
|
+
// resolve whether that callee ALWAYS returns a literal before falling
|
|
501
|
+
// through to the stricter "must equal the nearest literal assignment"
|
|
502
|
+
// check below, which a call-shaped RHS can never satisfy on its own.
|
|
503
|
+
if (lastAnyRhs && /^[A-Za-z_]\w*\s*\([^()]*\)$/.test(lastAnyRhs)) {
|
|
504
|
+
const calleeName = lastAnyRhs.slice(0, lastAnyRhs.indexOf('(')).trim();
|
|
505
|
+
if (_resolveCalleeReturnIsLiteral(content, calleeName, ranges, _calleeDepth)) return true;
|
|
506
|
+
}
|
|
507
|
+
// W4.J38 — the nearest assignment's own RHS is a bare identifier "copy"
|
|
508
|
+
// of ANOTHER variable (`dataCopy = data;` … later `String data =
|
|
509
|
+
// dataCopy;`) — Juliet's own "make a copy of data within the same
|
|
510
|
+
// method" flow variant. Resolve the COPIED-FROM variable the same way,
|
|
511
|
+
// recursively, at THIS assignment's own position (not `beforeIdx`) so the
|
|
512
|
+
// recursive scan sees only what was visible AT THE TIME of the copy, not
|
|
513
|
+
// anything assigned later. Bounded by the shared recursion-depth guard;
|
|
514
|
+
// excludes `null` explicitly (never a candidate literal — mirrors
|
|
515
|
+
// ldap-injection.js's own W4.C37 precedent for the identical reasoning).
|
|
516
|
+
if (lastAnyRhs && lastAnyRhs !== 'null' && /^[A-Za-z_]\w*$/.test(lastAnyRhs) && lastAnyRhs !== varName) {
|
|
517
|
+
if (_nearestAssignIsLiteral(content, lastAnyRhs, lastAnyIdx, ranges, (_calleeDepth || 0) + 1, siblingFiles)) return true;
|
|
518
|
+
}
|
|
519
|
+
if (lastLiteralEnd === -1 || lastLiteralEnd !== lastAnyEnd) {
|
|
520
|
+
// W4.J38 — before failing closed, check whether `varName` is a
|
|
521
|
+
// PARAMETER of the method enclosing `beforeIdx` and, if so, whether
|
|
522
|
+
// EVERY real call site of that method supplies a literal at the
|
|
523
|
+
// matching argument position. See _resolveParamLiteralViaAllCallSites's
|
|
524
|
+
// own header comment for why this is a DIFFERENT idiom than the
|
|
525
|
+
// paramRe guard below (a caller whose own literal sits AFTER the sink
|
|
526
|
+
// method in raw file text — invisible to any backward-only scan).
|
|
527
|
+
//
|
|
528
|
+
// W5.34 — a SIBLING idiom to both W4.J38 shapes above: Juliet's own
|
|
529
|
+
// "Flow Variant 45: data passed as a private class member variable from
|
|
530
|
+
// one function to another in the same class" (confirmed via the public
|
|
531
|
+
// mirror's own CWE80_XSS__CWE182_Servlet_getQueryString_Servlet_45.java)
|
|
532
|
+
// writes `dataGoodG2B = data;` in ONE method and reads it back via
|
|
533
|
+
// `String data = dataGoodG2B;` in a DIFFERENT method. The read's own
|
|
534
|
+
// recursive resolution correctly identifies `dataGoodG2B` as a bare
|
|
535
|
+
// identifier and recurses into it — but if the WRITER method
|
|
536
|
+
// (`goodG2B()`) is textually declared AFTER the reader method
|
|
537
|
+
// (`goodG2BSink()`), which Juliet's own file layout does not guarantee
|
|
538
|
+
// either way, the field's only assignment sits AFTER `beforeIdx` and is
|
|
539
|
+
// invisible to a backward-only scan, exactly the same "forward
|
|
540
|
+
// reference" problem `_resolveParamLiteralViaAllCallSites` exists to
|
|
541
|
+
// solve for parameters — just for a FIELD instead of an argument.
|
|
542
|
+
// `_resolveFieldLiteralViaAllAssignments` is that same fix for fields:
|
|
543
|
+
// it only fires when `varName` is confirmed to be an actual field
|
|
544
|
+
// declaration (an access-modifier-qualified declaration, syntax no
|
|
545
|
+
// local variable or parameter can carry), then requires EVERY
|
|
546
|
+
// assignment to it anywhere in the file to resolve to a literal.
|
|
547
|
+
return _resolveParamLiteralViaAllCallSites(content, varName, beforeIdx, deadRanges, _calleeDepth, siblingFiles)
|
|
548
|
+
|| _resolveFieldLiteralViaAllAssignments(content, varName, deadRanges, _calleeDepth);
|
|
549
|
+
}
|
|
550
|
+
// Juliet's "data passed as an argument from one method to another" flow
|
|
551
|
+
// variants (its own template naming: sources-sink-41+) sink INSIDE A
|
|
552
|
+
// HELPER method that receives the value as a formal PARAMETER, not a
|
|
553
|
+
// local assignment — `private void goodG2BSink(String data) { …
|
|
554
|
+
// DriverManager.getConnection(url, user, data); }`. A backward scan for
|
|
555
|
+
// "nearest assignment to `data`" then crosses OUT of that helper and into
|
|
556
|
+
// whichever CALLER happens to have last assigned a same-named `data`
|
|
557
|
+
// textually earlier in the file — frequently `bad()`'s own hardcoded
|
|
558
|
+
// literal, even when the ACTUAL call reaching this specific helper came
|
|
559
|
+
// from the SAFE `goodG2B()` path. A parameter declaration for `varName`
|
|
560
|
+
// that is MORE RECENT than the literal assignment means we've crossed a
|
|
561
|
+
// method boundary the assignment can't have followed us through, so the
|
|
562
|
+
// value here is actually unknown (determined by whichever caller this
|
|
563
|
+
// is) — fails closed, same direction as every other evidence-required
|
|
564
|
+
// check in this file.
|
|
565
|
+
const paramRe = new RegExp(`\\([^()]*\\b[\\w.<>\\[\\]]+\\s+${escaped}\\s*[,)]`, 'g');
|
|
566
|
+
let lastParamEnd = -1;
|
|
567
|
+
while ((m = paramRe.exec(content)) && m.index < beforeIdx) lastParamEnd = m.index + m[0].length;
|
|
568
|
+
return lastParamEnd <= lastLiteralEnd;
|
|
569
|
+
}
|
|
570
|
+
|
|
96
571
|
// Generic tainted-context indicator: file contains a known source.
|
|
97
572
|
// Includes Juliet's connect_tcp / Environment / Property variants.
|
|
98
573
|
const TAINTED_CONTEXT_RE = /\bSystem\.getenv\s*\(|\bSystem\.getProperty\s*\(|\brequest\s*\.\s*get(?:Parameter|Header|InputStream|Reader|QueryString|Cookies)\b|\bnew\s+Socket\s*\(|\b\w+\s*\.\s*getInputStream\s*\(\s*\)|\.readLine\s*\(\s*\)/;
|
|
@@ -511,6 +986,11 @@ export function scanJavaBenchExtras(file, raw) {
|
|
|
511
986
|
if (!JAVA_EXT.test(file) || !raw || raw.length > 500_000) return [];
|
|
512
987
|
const content = blankComments(raw);
|
|
513
988
|
const findings = [];
|
|
989
|
+
// See _nearestAssignIsLiteral's own header comment (W4.J33): lets that
|
|
990
|
+
// helper skip an assignment inside a provably-dead if(true)/if(false)
|
|
991
|
+
// branch when deciding whether the value reaching a sink is literal-only.
|
|
992
|
+
let deadRanges = [];
|
|
993
|
+
try { deadRanges = deadBranchRanges(raw); } catch { /* parse error → no AST info */ }
|
|
514
994
|
|
|
515
995
|
function lineOf(idx) { return content.substring(0, idx).split('\n').length; }
|
|
516
996
|
function isTainted(arg) { return TAINTED_HINT.test(arg); }
|
|
@@ -521,8 +1001,24 @@ export function scanJavaBenchExtras(file, raw) {
|
|
|
521
1001
|
let m;
|
|
522
1002
|
while ((m = SEND_REDIRECT_RE.exec(content))) {
|
|
523
1003
|
const arg = (m[1] || '').trim();
|
|
1004
|
+
// SARD_80_F1 W5.35 — a sink call sitting INSIDE a provably-dead branch
|
|
1005
|
+
// (Juliet's "switch(8){case 7: <tainted sendRedirect>}" idiom, Flow
|
|
1006
|
+
// Variant 15's goodB2G shapes — case 7 never matches scrutinee 8) is
|
|
1007
|
+
// unreachable regardless of whether the argument is tainted: the
|
|
1008
|
+
// literal-check below can never suppress it, since the value genuinely
|
|
1009
|
+
// IS tainted, just unreachable. `deadRanges` was already computed for
|
|
1010
|
+
// that literal check; nothing had checked it against the SINK's own
|
|
1011
|
+
// line until now.
|
|
1012
|
+
if (deadRanges.length && isLineInDeadRange(lineOf(m.index), deadRanges)) continue;
|
|
524
1013
|
// Literal-only arg: suppress. Tainted-looking arg: flag.
|
|
525
1014
|
if (/^"[^"]*"$/.test(arg)) continue; // pure literal — safe
|
|
1015
|
+
// Same literal-blindness class already fixed for SQLi/LDAP/XSS
|
|
1016
|
+
// (W4.J12/J13/J21), never ported here: Juliet's own convention keeps the
|
|
1017
|
+
// IDENTICAL `response.sendRedirect(data)` sink line in both `bad()` and
|
|
1018
|
+
// `goodG2B()`, only swapping `data`'s source — confirmed against the
|
|
1019
|
+
// public mirror (`CWE601_Open_Redirect__Servlet_PropertiesFile_01.java`:
|
|
1020
|
+
// `data = "foo";` in `goodG2B()`, then `response.sendRedirect(data);`).
|
|
1021
|
+
if (/^[A-Za-z_]\w*$/.test(arg) && _nearestAssignIsLiteral(content, arg, m.index, deadRanges)) continue;
|
|
526
1022
|
findings.push({
|
|
527
1023
|
id: id('java-extras:open-redirect', lineOf(m.index), m.index),
|
|
528
1024
|
kind: 'sast',
|
|
@@ -571,12 +1067,18 @@ export function scanJavaBenchExtras(file, raw) {
|
|
|
571
1067
|
}
|
|
572
1068
|
|
|
573
1069
|
// Pattern C: raw outbound Socket reading sensitive data. Matches Juliet's
|
|
574
|
-
// connect_tcp_* /
|
|
1070
|
+
// connect_tcp_* / send_* CWE-319 variants.
|
|
575
1071
|
if (fileHasSensitiveContext && fileHasSocketRead) {
|
|
576
1072
|
RAW_SOCKET_RE.lastIndex = 0;
|
|
577
1073
|
while ((m = RAW_SOCKET_RE.exec(content))) {
|
|
578
1074
|
emitCwe319(lineOf(m.index), m.index, 'cleartext Socket with sensitive-data context');
|
|
579
1075
|
}
|
|
1076
|
+
// Pattern C2: server-side ServerSocket.accept() — Juliet's listen_tcp_*
|
|
1077
|
+
// variants (see SERVERSOCKET_ACCEPT_RE's header comment).
|
|
1078
|
+
SERVERSOCKET_ACCEPT_RE.lastIndex = 0;
|
|
1079
|
+
while ((m = SERVERSOCKET_ACCEPT_RE.exec(content))) {
|
|
1080
|
+
emitCwe319(lineOf(m.index), m.index, 'cleartext ServerSocket.accept() with sensitive-data context');
|
|
1081
|
+
}
|
|
580
1082
|
}
|
|
581
1083
|
|
|
582
1084
|
// CWE-315 — sensitive Cookie without secure flag
|
|
@@ -653,6 +1155,32 @@ export function scanJavaBenchExtras(file, raw) {
|
|
|
653
1155
|
}
|
|
654
1156
|
}
|
|
655
1157
|
|
|
1158
|
+
// CWE-259 — hardcoded password reaching a credential parameter. See the
|
|
1159
|
+
// header comment above the regex constants for the three sink shapes and
|
|
1160
|
+
// why this fires on PROVABLE-CONSTANT values rather than tainted ones.
|
|
1161
|
+
const cwe259Lines = new Set();
|
|
1162
|
+
function emitHardcodedPassword(varName, matchIndex) {
|
|
1163
|
+
const L = lineOf(matchIndex);
|
|
1164
|
+
if (cwe259Lines.has(L)) return;
|
|
1165
|
+
if (!_nearestAssignIsLiteral(content, varName, matchIndex, deadRanges)) return;
|
|
1166
|
+
cwe259Lines.add(L);
|
|
1167
|
+
findings.push({
|
|
1168
|
+
id: id('java-extras:hardcoded-password', L, matchIndex),
|
|
1169
|
+
kind: 'sast',
|
|
1170
|
+
severity: 'high',
|
|
1171
|
+
vuln: 'Hardcoded Password used as credential',
|
|
1172
|
+
cwe: 'CWE-259', stride: 'Information Disclosure',
|
|
1173
|
+
file, line: L,
|
|
1174
|
+
snippet: content.substring(content.lastIndexOf('\n', matchIndex)+1, content.indexOf('\n', matchIndex)).trim().slice(0, 200),
|
|
1175
|
+
remediation: 'Never hardcode credentials. Load them from a secrets manager or environment variable at runtime.',
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
for (const re of [HARDCODED_PW_DRIVERMANAGER_RE, HARDCODED_PW_KERBEROSKEY_RE, HARDCODED_PW_PASSWORDAUTH_RE]) {
|
|
1179
|
+
re.lastIndex = 0;
|
|
1180
|
+
let hm;
|
|
1181
|
+
while ((hm = re.exec(content))) emitHardcodedPassword(hm[1], hm.index);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
656
1184
|
return findings;
|
|
657
1185
|
}
|
|
658
1186
|
|
|
@@ -81,6 +81,29 @@ import { blankComments } from './_comment-strip.js';
|
|
|
81
81
|
|
|
82
82
|
function _lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
|
|
83
83
|
|
|
84
|
+
// SARD_80_F1_SCANNER_PRD.md Juliet Java audit: `hasOIS` (file-wide) plus a
|
|
85
|
+
// bare `\w+.readObject()` anywhere in the same file was measured at 122 FPs
|
|
86
|
+
// on real corpus fixtures — the file-wide check does not require the CALL's
|
|
87
|
+
// own receiver to be the tracked ObjectInputStream variable, so a file that
|
|
88
|
+
// merely constructs an OIS somewhere (even for a genuinely unrelated
|
|
89
|
+
// CWE's own source idiom) makes every OTHER bare `.readObject()` call in
|
|
90
|
+
// the file match too, regardless of that call's own receiver type. Track
|
|
91
|
+
// the actual variable name(s) assigned from `new ObjectInputStream(...)`
|
|
92
|
+
// and require the receiver to be one of them — real correlation between
|
|
93
|
+
// the construction and the call, not file-wide co-occurrence.
|
|
94
|
+
const OIS_VAR_RE = /\bObjectInputStream\s+(\w+)\s*=\s*new\s+ObjectInputStream\b|\b(\w+)\s*=\s*new\s+ObjectInputStream\b/g;
|
|
95
|
+
|
|
96
|
+
function _objectInputStreamVars(code) {
|
|
97
|
+
const vars = new Set();
|
|
98
|
+
let m;
|
|
99
|
+
const re = new RegExp(OIS_VAR_RE.source, OIS_VAR_RE.flags);
|
|
100
|
+
while ((m = re.exec(code))) {
|
|
101
|
+
const name = m[1] || m[2];
|
|
102
|
+
if (name) vars.add(name);
|
|
103
|
+
}
|
|
104
|
+
return vars;
|
|
105
|
+
}
|
|
106
|
+
|
|
84
107
|
export function scanJavaDeserialization(fp, raw) {
|
|
85
108
|
if (!/\.(?:java|kt|kts|scala|groovy|gradle)$/i.test(fp)) return [];
|
|
86
109
|
if (!raw || raw.length > 500_000) return [];
|
|
@@ -89,6 +112,7 @@ export function scanJavaDeserialization(fp, raw) {
|
|
|
89
112
|
// one, a bare .readObject() call is almost certainly RMI/JDBC/socket noise
|
|
90
113
|
// unrelated to native serialization gadgets.
|
|
91
114
|
const hasOIS = /\bnew\s+ObjectInputStream\b/.test(code);
|
|
115
|
+
const oisVars = _objectInputStreamVars(code);
|
|
92
116
|
// Pre-pass for XStream — gate the broad `.fromXML(` regex.
|
|
93
117
|
const hasXStream = /\bXStream\b|\bxstream\.|com\.thoughtworks\.xstream/.test(code);
|
|
94
118
|
const findings = [];
|
|
@@ -104,8 +128,13 @@ export function scanJavaDeserialization(fp, raw) {
|
|
|
104
128
|
if (p.requireOIS && !hasOIS) continue;
|
|
105
129
|
// For the ObjectInputStream pattern, the second branch (bare \w+.readObject)
|
|
106
130
|
// only fires when there's an OIS in the file AND the method receiver isn't
|
|
107
|
-
// obviously a JDBC ResultSet (rs/results/rset names)
|
|
108
|
-
|
|
131
|
+
// obviously a JDBC ResultSet (rs/results/rset names) AND the receiver is
|
|
132
|
+
// actually a variable this file constructed as an ObjectInputStream —
|
|
133
|
+
// real correlation, not just file-wide co-occurrence (see oisVars above).
|
|
134
|
+
if (p.requireOIS && m[1]) {
|
|
135
|
+
if (/^(?:rs|result|results|rset|resultset)$/i.test(m[1])) continue;
|
|
136
|
+
if (!oisVars.has(m[1])) continue;
|
|
137
|
+
}
|
|
109
138
|
const line = _lineOf(raw, m.index);
|
|
110
139
|
push({
|
|
111
140
|
id: `java-deser:${fp}:${line}:${p.name.replace(/\s+/g, '_')}`,
|