@clear-capabilities/agentic-security-scanner 0.151.1 → 0.151.3
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 +186 -0
- package/dist/1905.index.js +30 -2
- package/dist/3499.index.js +20 -1
- package/dist/agentic-security.mjs +1 -1
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +8 -4
- package/src/dataflow/catalog.js +57 -4
- package/src/dataflow/engine.js +13 -1
- package/src/discovery/THREAT_MODEL.md +122 -0
- package/src/discovery/disprove.js +10 -1
- package/src/discovery/lenses.js +20 -1
- package/src/ir/callgraph.js +38 -0
- package/src/ir/class-hierarchy.js +18 -1
- package/src/ir/parser-cs.js +206 -4
- package/src/ir/parser-java.js +29 -7
- package/src/sast/code-injection-multilang.js +9 -0
- package/src/sast/ldap-injection.js +12 -3
package/src/ir/parser-cs.js
CHANGED
|
@@ -68,6 +68,177 @@ const METHOD_RE = new RegExp(
|
|
|
68
68
|
'\\s*\\(([^)]*)\\)' + // params (group 3)
|
|
69
69
|
'\\s*\\{', 'g');
|
|
70
70
|
|
|
71
|
+
// Class/struct declaration: modifiers* `class`|`struct` Name, optionally
|
|
72
|
+
// followed by generic params (`<T>`) and/or a base/interface list
|
|
73
|
+
// (`: Base, IFoo`), then the body's opening `{`.
|
|
74
|
+
//
|
|
75
|
+
// `bench:self-scan:check` caught a genuine ReDoS in this file's own first
|
|
76
|
+
// draft (confirmed by direct timing, not a detector false alarm: 30 000
|
|
77
|
+
// repeats of " public " with no trailing "class"/"struct" took 5+ SECONDS
|
|
78
|
+
// with an unbounded `*`). Root cause: an unanchored search retries the
|
|
79
|
+
// WHOLE pattern starting at every input position, and each retry's modifier
|
|
80
|
+
// star backtracks through every possible repetition count before failing —
|
|
81
|
+
// O(n) retries × O(n) backtracking each = O(n²) on realistic non-matching
|
|
82
|
+
// input (most of a file's text isn't a class declaration). Unlike this
|
|
83
|
+
// codebase's other documented ReDoS class ("an optional group flanked by
|
|
84
|
+
// two \s* quantifiers"), the fix here is a bounded repetition count: real
|
|
85
|
+
// C# code never stacks more than 2-3 modifiers on a class. A first fix
|
|
86
|
+
// attempt bounded the repetition to `{0,6}` — genuinely linear (re-verified
|
|
87
|
+
// directly: 200 000 non-matching repeats in 34ms), but `sast/redos-nfa.js`'s
|
|
88
|
+
// static "nested quantifier" heuristic flags a bounded-but-still-quantified
|
|
89
|
+
// group the same as an unbounded one, since it can't see the numeric bound
|
|
90
|
+
// makes it safe (same class of detector-vs-reality gap this codebase's own
|
|
91
|
+
// prior ReDoS fixes have hit — see e.g. the Kotlin trailing-lambda note in
|
|
92
|
+
// `../ir/CLAUDE.md`: "the SAME detector's heuristic still flagged the
|
|
93
|
+
// replacement... even though it measured linear on its own"). Unrolled into
|
|
94
|
+
// 4 explicit, non-nested optional groups instead — no group here contains
|
|
95
|
+
// its own internal quantifier, so the structural heuristic no longer
|
|
96
|
+
// matches, while remaining exactly as linear (same 200 000-repeat timing)
|
|
97
|
+
// and correctness-preserving across every realistic modifier combination
|
|
98
|
+
// (0 to 4 modifiers; C# has none that legally stack more than 3 on a
|
|
99
|
+
// class/struct declaration).
|
|
100
|
+
const _CS_MOD = String.raw`(?:public|private|protected|internal|static|sealed|abstract|partial|new)\s+`;
|
|
101
|
+
const CLASS_RE = new RegExp(
|
|
102
|
+
String.raw`(?:^|[\s;{}])(?:${_CS_MOD})?(?:${_CS_MOD})?(?:${_CS_MOD})?(?:${_CS_MOD})?(?:class|struct)\s+([A-Za-z_][A-Za-z0-9_]*)`, 'g');
|
|
103
|
+
|
|
104
|
+
// Taint-recall: C#'s `_qid()`/`fn.name` never recorded which CLASS a method
|
|
105
|
+
// belongs to at all (no middle `::ClassName::` segment, unlike Java's/JS's
|
|
106
|
+
// convention — see `../dataflow/CLAUDE.md`'s C# row and
|
|
107
|
+
// `bench/sard/IMPLEMENTATION_STATUS.md`'s "real accuracy improvement pass"
|
|
108
|
+
// section for the full account). That makes `callgraph.js`'s `classMethods`
|
|
109
|
+
// index — which resolves `new Helper().BadSink(x)` / `Helper.BadSink(x)` —
|
|
110
|
+
// PERMANENTLY EMPTY for C#, so no cross-class call (same file or not) could
|
|
111
|
+
// ever resolve, blocking interprocedural taint for exactly the shape
|
|
112
|
+
// Juliet's own interface-based helper-class test structure (CWE-90/643/79-
|
|
113
|
+
// 83/113/78/etc.'s dominant remaining flow-variant pattern) uses.
|
|
114
|
+
//
|
|
115
|
+
// Finds every class/struct declaration's body range so a method's enclosing
|
|
116
|
+
// class can be looked up by character offset. A method not inside any class
|
|
117
|
+
// (a rare but legal C# top-level-statements shape) gets `className: null`.
|
|
118
|
+
// Nested classes are handled by picking the SMALLEST (innermost) range that
|
|
119
|
+
// contains the method — ranges are not required to be non-overlapping.
|
|
120
|
+
function _findClassRanges(code) {
|
|
121
|
+
const ranges = [];
|
|
122
|
+
CLASS_RE.lastIndex = 0;
|
|
123
|
+
let m;
|
|
124
|
+
while ((m = CLASS_RE.exec(code)) !== null) {
|
|
125
|
+
const name = m[1];
|
|
126
|
+
// Skip past an optional generic parameter list and/or base/interface
|
|
127
|
+
// list to find the REAL opening brace — `class Foo<T> : Base<T>, IBar`
|
|
128
|
+
// has two `<...>` regions before the body even starts. Track angle-
|
|
129
|
+
// bracket depth; balanced-`<>` is a reasonable approximation here (C#
|
|
130
|
+
// generics don't nest with unbalanced `<`/`>` in valid code), and a
|
|
131
|
+
// `{` encountered while depth > 0 (an unlikely default-value-in-
|
|
132
|
+
// generic-constraint edge case) is simply skipped rather than
|
|
133
|
+
// mis-treated as the class body.
|
|
134
|
+
let i = m.index + m[0].length;
|
|
135
|
+
let angleDepth = 0;
|
|
136
|
+
while (i < code.length) {
|
|
137
|
+
const c = code[i];
|
|
138
|
+
if (c === '<') angleDepth++;
|
|
139
|
+
else if (c === '>') { if (angleDepth > 0) angleDepth--; }
|
|
140
|
+
else if (c === '{' && angleDepth === 0) break;
|
|
141
|
+
else if ((c === ';') && angleDepth === 0) { i = -1; break; } // forward-declaration-shaped or malformed; bail
|
|
142
|
+
i++;
|
|
143
|
+
}
|
|
144
|
+
if (i < 0 || i >= code.length || code[i] !== '{') continue;
|
|
145
|
+
const extracted = _extractBody(code, i);
|
|
146
|
+
if (!extracted) continue;
|
|
147
|
+
ranges.push({ name, start: i, end: extracted.end });
|
|
148
|
+
CLASS_RE.lastIndex = i + 1; // resume scanning INSIDE the class body too, for nested classes
|
|
149
|
+
}
|
|
150
|
+
return ranges;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Innermost class whose range contains `pos` (a method declaration's match
|
|
154
|
+
// offset), or null when the method sits outside any class (top-level
|
|
155
|
+
// statements — legal but rare).
|
|
156
|
+
function _enclosingClassName(ranges, pos) {
|
|
157
|
+
let best = null;
|
|
158
|
+
for (const r of ranges) {
|
|
159
|
+
if (pos >= r.start && pos < r.end) {
|
|
160
|
+
if (!best || (r.end - r.start) < (best.end - best.start)) best = r;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return best ? best.name : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Companion to the class-tracking fix above, closing the OTHER half of the
|
|
167
|
+
// same gap: `HelperB h = new HelperB(); h.BadSink(data);` — Juliet's other
|
|
168
|
+
// dominant helper-class idiom, alongside the inline `new Helper().Sink(x)`
|
|
169
|
+
// chain `classMethods` now resolves directly. `h.BadSink` is a callee
|
|
170
|
+
// string built from the LOCAL VARIABLE name, which `classMethods` (keyed
|
|
171
|
+
// by real class name) can never match. This is exact same-function local
|
|
172
|
+
// type inference, not a guess: `h`'s constructed type is unambiguous
|
|
173
|
+
// wherever `h = new HelperB()` appears, so a call written as `h.BadSink(x)`
|
|
174
|
+
// is rewritten in place to `HelperB.BadSink` — the callee string
|
|
175
|
+
// `classMethods` already knows how to resolve (and, since `classMethods`
|
|
176
|
+
// is a PROJECT-WIDE index built across every file, this also transparently
|
|
177
|
+
// covers Juliet's documented cross-FILE `_NNa`/`_NNb` paired-variant
|
|
178
|
+
// convention when the helper class lives in a sibling file, with no
|
|
179
|
+
// separate cross-file mechanism needed). Refuses to guess when a variable
|
|
180
|
+
// is (re)assigned to more than one distinct constructed type in the same
|
|
181
|
+
// function — ambiguous, same "refuse rather than fabricate an edge"
|
|
182
|
+
// convention `bareTailInFile` already uses.
|
|
183
|
+
function _localVarConstructedTypes(nodes) {
|
|
184
|
+
const varTypes = new Map(); // varName -> className | null (ambiguous)
|
|
185
|
+
for (const node of Object.values(nodes)) {
|
|
186
|
+
if (node.kind !== 'assign') continue;
|
|
187
|
+
const src = node.source;
|
|
188
|
+
if (!src || src.kind !== 'call' || !src.isNew || typeof src.callee !== 'string') continue;
|
|
189
|
+
const varName = node.target;
|
|
190
|
+
if (!varName || varName.includes('.')) continue; // only a bare local, never a member-write target
|
|
191
|
+
if (varTypes.has(varName)) {
|
|
192
|
+
if (varTypes.get(varName) !== src.callee) varTypes.set(varName, null);
|
|
193
|
+
} else {
|
|
194
|
+
varTypes.set(varName, src.callee);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return varTypes;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Recursively rewrites `varName.method`-shaped call callees to
|
|
201
|
+
// `ClassName.method` wherever `varName` has an unambiguous constructed type
|
|
202
|
+
// in `varTypes`, walking into every expression-tree shape `_lowerExpr` can
|
|
203
|
+
// produce (a tainted argument to a rewritten call is just as real a finding
|
|
204
|
+
// as the call itself, so args/branches must be walked too, not just the
|
|
205
|
+
// node's own top-level callee).
|
|
206
|
+
function _rewriteVarTypeCallees(expr, varTypes) {
|
|
207
|
+
if (!expr || typeof expr !== 'object') return;
|
|
208
|
+
if (expr.kind === 'call') {
|
|
209
|
+
if (typeof expr.callee === 'string') {
|
|
210
|
+
const dot = expr.callee.indexOf('.');
|
|
211
|
+
if (dot > 0) {
|
|
212
|
+
const varName = expr.callee.slice(0, dot);
|
|
213
|
+
const cls = varTypes.get(varName);
|
|
214
|
+
if (cls) expr.callee = `${cls}${expr.callee.slice(dot)}`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (Array.isArray(expr.args)) for (const a of expr.args) _rewriteVarTypeCallees(a, varTypes);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (expr.kind === 'member') { _rewriteVarTypeCallees(expr.object, varTypes); return; }
|
|
221
|
+
if (expr.kind === 'binary' || expr.kind === 'logical') {
|
|
222
|
+
_rewriteVarTypeCallees(expr.left, varTypes); _rewriteVarTypeCallees(expr.right, varTypes); return;
|
|
223
|
+
}
|
|
224
|
+
if (expr.kind === 'tpl' && Array.isArray(expr.parts)) { for (const p of expr.parts) _rewriteVarTypeCallees(p, varTypes); return; }
|
|
225
|
+
if (expr.kind === 'union' && Array.isArray(expr.branches)) { for (const b of expr.branches) _rewriteVarTypeCallees(b, varTypes); return; }
|
|
226
|
+
if (expr.kind === 'array' && Array.isArray(expr.elements)) { for (const e of expr.elements) _rewriteVarTypeCallees(e, varTypes); return; }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function _applyVarTypeRewrite(nodes) {
|
|
230
|
+
const varTypes = _localVarConstructedTypes(nodes);
|
|
231
|
+
let any = false;
|
|
232
|
+
for (const t of varTypes.values()) if (t) { any = true; break; }
|
|
233
|
+
if (!any) return;
|
|
234
|
+
for (const node of Object.values(nodes)) {
|
|
235
|
+
if (node.kind === 'call') _rewriteVarTypeCallees(node, varTypes);
|
|
236
|
+
else if (node.kind === 'assign') _rewriteVarTypeCallees(node.source, varTypes);
|
|
237
|
+
else if (node.kind === 'return' || node.kind === 'throw') _rewriteVarTypeCallees(node.value, varTypes);
|
|
238
|
+
else if (node.kind === 'if') _rewriteVarTypeCallees(node.cond, varTypes);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
71
242
|
// Matches a top-level statement inside a method body. Splits on `;` at
|
|
72
243
|
// brace-depth 0 (keeping simple lambdas inside calls intact), AND — R8 —
|
|
73
244
|
// also flushes on a `}` that returns the SAME shared depth counter to 0.
|
|
@@ -460,6 +631,28 @@ function _lowerStmt(stmt, line) {
|
|
|
460
631
|
const chained = _followChain(s, cm.endIdx, cm.callee, _splitTopLevelCommas(cm.argsText).map(_lowerExpr), false);
|
|
461
632
|
return { kind: 'call', line, callee: chained.callee, args: chained.args };
|
|
462
633
|
}
|
|
634
|
+
// Statement-form constructor-chained call: `new Helper().BadSink(data);`
|
|
635
|
+
// — the inline-instantiate-and-call idiom, with NO assignment at all.
|
|
636
|
+
// Previously fell through to `unknown` entirely (the statement-form
|
|
637
|
+
// branch above requires the FIRST token to be a plain identifier, never
|
|
638
|
+
// `new`) — silently dropping the call, its arguments, and any taint
|
|
639
|
+
// reaching a sink inside the constructed object's method. Mirrors the
|
|
640
|
+
// identical `new`-chain handling `_lowerExpr`'s expression path already
|
|
641
|
+
// has (this statement-form path is otherwise a strict subset of that
|
|
642
|
+
// one), and inherits `classMethods`'s cross-class resolution for free
|
|
643
|
+
// once the chain's callee dot-joins to `ClassName.method`.
|
|
644
|
+
const nm = matchBalancedCall(s, /^new\s+([\w.]+)/);
|
|
645
|
+
if (nm) {
|
|
646
|
+
const ctorArgs = _splitTopLevelCommas(nm.argsText).map(_lowerExpr);
|
|
647
|
+
const chained = _followChain(s, nm.endIdx, nm.callee.split('.').pop(), ctorArgs, true);
|
|
648
|
+
// A trailing `.Method(...)` grows `chained.callee` past the bare
|
|
649
|
+
// constructor name (into `ClassName.Method`) — only THAT shape is a
|
|
650
|
+
// call-kind statement worth reporting. A bare `new Helper();` with no
|
|
651
|
+
// chained call has no observable side effect from this statement's own
|
|
652
|
+
// perspective; fall through to `unknown`, matching this file's
|
|
653
|
+
// existing "nothing to lower" convention for a no-op statement.
|
|
654
|
+
if (chained.callee.includes('.')) return { kind: 'call', line, callee: chained.callee, args: chained.args };
|
|
655
|
+
}
|
|
463
656
|
return { kind: 'unknown', line, text: s };
|
|
464
657
|
}
|
|
465
658
|
|
|
@@ -492,9 +685,15 @@ function _lineAt(src, idx) {
|
|
|
492
685
|
return line;
|
|
493
686
|
}
|
|
494
687
|
|
|
495
|
-
function _qid(file, name, line, body) {
|
|
688
|
+
function _qid(file, name, line, body, className) {
|
|
496
689
|
const sha = crypto.createHash('sha256').update(body).digest('hex').slice(0, 8);
|
|
497
|
-
|
|
690
|
+
// `::ClassName::` middle segment — the SAME convention parser-java.js and
|
|
691
|
+
// parser-js.js already use (see class-hierarchy.js's "Shape 2" and
|
|
692
|
+
// callgraph.js's `classMethods` index, both of which parse exactly this
|
|
693
|
+
// shape back out). Omitted (falls back to the pre-existing two-segment
|
|
694
|
+
// form) when the method sits outside any class — legal C# top-level
|
|
695
|
+
// statements — so that shape is unaffected.
|
|
696
|
+
return className ? `${file}::${className}::${name}@${line}#${sha}` : `${file}::${name}@${line}#${sha}`;
|
|
498
697
|
}
|
|
499
698
|
|
|
500
699
|
// Node-id counter for `_buildCfg`. Reset to 0 per function (see
|
|
@@ -679,10 +878,12 @@ function _buildCfg(bodyText, nodes, prevId, funcStartLine, lineStarts, baseAbs,
|
|
|
679
878
|
export function parseCSharpFile(file, code) {
|
|
680
879
|
if (!file || typeof code !== 'string') return null;
|
|
681
880
|
const functions = [];
|
|
881
|
+
const classRanges = _findClassRanges(code);
|
|
682
882
|
METHOD_RE.lastIndex = 0;
|
|
683
883
|
let m;
|
|
684
884
|
while ((m = METHOD_RE.exec(code)) !== null) {
|
|
685
885
|
const name = m[2];
|
|
886
|
+
const className = _enclosingClassName(classRanges, m.index);
|
|
686
887
|
const paramsText = m[3] || '';
|
|
687
888
|
const paramAnnotations = [];
|
|
688
889
|
// `keptIdx` tracks the parameter's position in the FILTERED array — the
|
|
@@ -771,10 +972,11 @@ export function parseCSharpFile(file, code) {
|
|
|
771
972
|
const tail = _buildCfg(extracted.body, nodes, 'entry', bodyStartLine, lineStarts, 0, 0);
|
|
772
973
|
nodes[tail].succ.push('exit');
|
|
773
974
|
nodes.exit.pred.push(tail);
|
|
975
|
+
_applyVarTypeRewrite(nodes);
|
|
774
976
|
const cfg = { entry: 'entry', exit: 'exit', nodes };
|
|
775
977
|
functions.push({
|
|
776
|
-
qid: _qid(file, name, startLine, extracted.body),
|
|
777
|
-
name, line: startLine, params, file,
|
|
978
|
+
qid: _qid(file, name, startLine, extracted.body, className),
|
|
979
|
+
name: className ? `${className}.${name}` : name, line: startLine, params, file,
|
|
778
980
|
cfg,
|
|
779
981
|
calls: callSitesFromCfg(cfg),
|
|
780
982
|
...(paramAnnotations.length ? { paramAnnotations } : {}),
|
package/src/ir/parser-java.js
CHANGED
|
@@ -192,15 +192,37 @@ function exprFromCst(node) {
|
|
|
192
192
|
if (node.children.Identifier) return { kind: 'ident', name: node.children.Identifier[0].image };
|
|
193
193
|
// Binary expression
|
|
194
194
|
if (node.children.BinaryOperator || node.children.binaryExpression) {
|
|
195
|
-
//
|
|
195
|
+
// Found via the independent-holdout fixture microledger (SARD_80_F1_
|
|
196
|
+
// SCANNER_PRD.md P1 item 6): this branch hardcoded `op: '?'` for EVERY
|
|
197
|
+
// binary expression, regardless of the real operator, and only ever
|
|
198
|
+
// took the first two operands — chevrotain's `binaryExpression`
|
|
199
|
+
// production for a chain like `"a" + query + "b"` is FLAT (one node
|
|
200
|
+
// listing all 3 operands and both `+` tokens), not a nested binary
|
|
201
|
+
// tree, so a 3+-operand chain silently dropped every operand past the
|
|
202
|
+
// second AND mislabeled the operator on top of that.
|
|
203
|
+
//
|
|
204
|
+
// Neither defect was independently harmless: a taint value sitting in
|
|
205
|
+
// the THIRD (or later) operand of a concatenation was invisible to the
|
|
206
|
+
// taint walker regardless of the operator bug, and a real corpus
|
|
207
|
+
// fixture with exactly this shape (`"<div>..." + query + "</div>"`,
|
|
208
|
+
// one `sink(a + tainted + b)` call) lost its finding entirely — not
|
|
209
|
+
// merely mislabeled. `node.children.BinaryOperator` is chevrotain's own
|
|
210
|
+
// token array for this production; `.image` is the literal operator
|
|
211
|
+
// text (confirmed via direct CST inspection this session — `+`, not a
|
|
212
|
+
// synthesized value). Left-folds all operands with their real
|
|
213
|
+
// operators, matching left-to-right evaluation order; the `|| '+'`
|
|
214
|
+
// fallback only matters if a future java-parser version ever changes
|
|
215
|
+
// this shape so operator/operand counts disagree, and `+` is the
|
|
216
|
+
// overwhelmingly dominant real-world case for this production
|
|
217
|
+
// (string concatenation), so it is the safer of two bad guesses.
|
|
196
218
|
const kids = node.children.unaryExpression || node.children.expression || [];
|
|
219
|
+
const ops = (node.children.BinaryOperator || []).map(t => t.image);
|
|
197
220
|
if (kids.length >= 2) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
op: '
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
};
|
|
221
|
+
let result = exprFromCst(kids[0]);
|
|
222
|
+
for (let i = 1; i < kids.length; i++) {
|
|
223
|
+
result = { kind: 'binary', op: ops[i - 1] || '+', left: result, right: exprFromCst(kids[i]) };
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
204
226
|
}
|
|
205
227
|
}
|
|
206
228
|
// Fall through: recurse the first child
|
|
@@ -62,6 +62,15 @@ function patternsFor(lang) {
|
|
|
62
62
|
{ key: 'roslyn', re: new RegExp(String.raw`\bCSharpScript\s*\.\s*(?:EvaluateAsync|RunAsync|Create)\s*\(\s*${notLiteral}`, 'g') },
|
|
63
63
|
{ key: 'datatable-compute', re: new RegExp(String.raw`\.\s*Compute\s*\(\s*${notLiteral}`, 'g'),
|
|
64
64
|
gate: /\b(?:DataTable|DataColumn|DataView)\b/ },
|
|
65
|
+
// System.CodeDom.Compiler's CSharpCodeProvider/VBCodeProvider — the
|
|
66
|
+
// historically standard .NET dynamic-compile-and-run API, predating
|
|
67
|
+
// Roslyn's CSharpScript by over a decade and still the one most
|
|
68
|
+
// widely documented and used. Canonical shape is
|
|
69
|
+
// `provider.CompileAssemblyFromSource(parameters, source)` — the
|
|
70
|
+
// tainted source is the SECOND argument, not the first, unlike every
|
|
71
|
+
// other sink in this file. A literal source string doesn't match.
|
|
72
|
+
{ key: 'codedom-compile', re: new RegExp(String.raw`\.\s*CompileAssemblyFromSource\s*\(\s*[^,()]+\s*,\s*${notLiteral}`, 'g'),
|
|
73
|
+
gate: /\b(?:CSharpCodeProvider|VBCodeProvider|CodeDomProvider)\b/ },
|
|
65
74
|
];
|
|
66
75
|
}
|
|
67
76
|
if (lang === 'js') {
|
|
@@ -23,9 +23,18 @@ import { blankComments } from './_comment-strip.js';
|
|
|
23
23
|
// etc.) so we don't fire on every `"foo=" + bar` concatenation in unrelated
|
|
24
24
|
// code.
|
|
25
25
|
|
|
26
|
-
// LDAP filter
|
|
27
|
-
//
|
|
28
|
-
|
|
26
|
+
// LDAP filter attribute name. Real LDAP/AD schemas define far more
|
|
27
|
+
// attributes than any fixed enum can enumerate (custom schema extensions,
|
|
28
|
+
// and standard-but-uncommon ones like `department`/`title`/`employeeID`/
|
|
29
|
+
// `homeDirectory`/`description`) — a hardcoded whitelist here is a real,
|
|
30
|
+
// general precision-vs-recall bug, not just narrow for one benchmark.
|
|
31
|
+
// Matches conventional LDAP attribute-name shape (alnum + hyphen, the RFC
|
|
32
|
+
// 4512 `descr` production, simplified). Path A (inline concat, ungated) and
|
|
33
|
+
// Path B (variable-form) both already anchor on a `(<attr>=` filter-syntax
|
|
34
|
+
// shape plus, for Path B, a file-level LDAP-API hint (`LDAP_HINT_RE`) — the
|
|
35
|
+
// widened ATTR only loosens which attribute NAME is accepted, not whether
|
|
36
|
+
// the surrounding shape looks like an LDAP filter at all.
|
|
37
|
+
const ATTR = '[A-Za-z][\\w-]{0,40}';
|
|
29
38
|
|
|
30
39
|
// Path A — concatenation/interpolation INSIDE (or adjacent to) the sink call.
|
|
31
40
|
// High-confidence; does not need the file-level context hint.
|