@clear-capabilities/agentic-security-scanner 0.151.2 → 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 +184 -0
- package/dist/1905.index.js +30 -2
- package/dist/3499.index.js +20 -1
- package/dist/agentic-security.mjs +80 -80
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +8 -4
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +842 -17
- package/src/dataflow/engine.js +865 -90
- 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/discovery/THREAT_MODEL.md +122 -0
- package/src/discovery/disprove.js +10 -1
- package/src/discovery/lenses.js +20 -1
- 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 +119 -7
- 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 +527 -24
- 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
package/src/sast/csharp.js
CHANGED
|
@@ -28,8 +28,12 @@
|
|
|
28
28
|
// CWE-502 insecure-deserialization BinaryFormatter.Deserialize / NetDataContractSerializer / Newtonsoft TypeNameHandling != None
|
|
29
29
|
// CWE-611 xxe XmlDocument w/o XmlResolver=null; XmlReaderSettings w/o DtdProcessing=Prohibit
|
|
30
30
|
// CWE-798 hardcoded-secret Field/local with name matching password/token/secret/apiKey + non-empty string literal initializer
|
|
31
|
+
// CWE-313 data-exposure File.WriteAllText/AppendAllText/writer.Write with a sensitive-named value (name-based, not taint-based)
|
|
32
|
+
// CWE-314 data-exposure Registry.SetValue / RegistryKey.SetValue with a sensitive-named value
|
|
33
|
+
// CWE-315 data-exposure new HttpCookie(...) / Cookies[...].Value = with a sensitive-named value
|
|
31
34
|
// CWE-1004 header-hardening new HttpCookie missing Secure/HttpOnly
|
|
32
35
|
// CWE-22 validate-input-false [ValidateInput(false)] attribute
|
|
36
|
+
// CWE-523 insecure-http hardcoded <form action='http://...'> submitting a password field (point flaw, not taint-based)
|
|
33
37
|
//
|
|
34
38
|
// Findings carry: id, file, line, vuln, severity, cwe, stride, snippet,
|
|
35
39
|
// remediation, confidence, parser, family, and a `_taintEvidence` field
|
|
@@ -78,7 +82,21 @@ const SQL_EXEC_METHODS = /^(?:Execute(?:Reader|Scalar|NonQuery|DbDataReader|Rea
|
|
|
78
82
|
const LDAP_SEARCH_TYPES = /^(?:DirectorySearcher|LdapConnection)$/;
|
|
79
83
|
const LDAP_SEARCH_METHODS = /^(?:Search|FindOne|FindAll|SendRequest)$/;
|
|
80
84
|
const WEAK_CRYPTO_TYPES = /^(?:DESCryptoServiceProvider|TripleDESCryptoServiceProvider|RC2CryptoServiceProvider|MD5CryptoServiceProvider|MD5Cng|SHA1CryptoServiceProvider|SHA1Managed|SHA1Cng|HMACSHA1|HMACMD5|DES|TripleDES|RC2|MD5|SHA1)$/;
|
|
81
|
-
const WEAK_CRYPTO_FACTORY_PATTERN = /\b(
|
|
85
|
+
const WEAK_CRYPTO_FACTORY_PATTERN = /\b(DES|TripleDES|RC2|MD5|SHA1)\.Create\b/;
|
|
86
|
+
// SARD_80_F1 W4.C15 — `WEAK_CRYPTO_TYPES`/`WEAK_CRYPTO_FACTORY_PATTERN` bundle
|
|
87
|
+
// two distinct CWEs under one hardcoded 'CWE-327' tag: CWE-327 is the general
|
|
88
|
+
// "risky crypto algorithm" (ciphers — DES/3DES/RC2), CWE-328 is specifically
|
|
89
|
+
// "Use of a Weak Hash" (MD5/SHA1 and their CryptoServiceProvider/Cng/Managed/
|
|
90
|
+
// HMAC variants). Real corpus verification (the C# Juliet suite's own
|
|
91
|
+
// CWE-327 vs CWE-328 directories, each with distinct gold expectations)
|
|
92
|
+
// confirmed every hash-shaped finding here was scored as a miss against
|
|
93
|
+
// BOTH directories: fp against CWE-327 (wrong tag), fn against CWE-328
|
|
94
|
+
// (never emitted). `_weakCryptoCwe(name)` decides the correct CWE per
|
|
95
|
+
// matched algorithm name so a single detector can emit both correctly.
|
|
96
|
+
const WEAK_HASH_NAMES = /^(?:MD5CryptoServiceProvider|MD5Cng|SHA1CryptoServiceProvider|SHA1Managed|SHA1Cng|HMACSHA1|HMACMD5|MD5|SHA1)$/;
|
|
97
|
+
function _weakCryptoCwe(name) {
|
|
98
|
+
return WEAK_HASH_NAMES.test(name) ? 'CWE-328' : 'CWE-327';
|
|
99
|
+
}
|
|
82
100
|
const SECRET_NAME_PATTERN = /^(?:password|passwd|pw|pwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|priv(?:ate)?[_-]?key|cred(?:ential)?s?|connection[_-]?string|conn[_-]?str)$/i;
|
|
83
101
|
const PATH_TRAVERSAL_BASES_SANITIZER = /\bPath\.GetFullPath\b/;
|
|
84
102
|
const XSS_SAFE_SINK_PATTERN = /\bHtmlEncode\b|\bHtmlEncoder\b|\bAntiXss/;
|
|
@@ -91,10 +109,14 @@ function makeFinding({ ruleId, file, line, raw, ir, family, severity, cwe, vuln,
|
|
|
91
109
|
: cwe === 'CWE-502' ? 'Elevation of Privilege'
|
|
92
110
|
: cwe === 'CWE-22' ? 'Tampering'
|
|
93
111
|
: cwe === 'CWE-327' ? 'Information Disclosure'
|
|
112
|
+
: cwe === 'CWE-328' ? 'Information Disclosure'
|
|
94
113
|
: cwe === 'CWE-330' ? 'Spoofing'
|
|
95
114
|
: cwe === 'CWE-798' ? 'Information Disclosure'
|
|
96
115
|
: cwe === 'CWE-1004'? 'Information Disclosure'
|
|
97
116
|
: cwe === 'CWE-90' ? 'Tampering'
|
|
117
|
+
: cwe === 'CWE-313' ? 'Information Disclosure'
|
|
118
|
+
: cwe === 'CWE-314' ? 'Information Disclosure'
|
|
119
|
+
: cwe === 'CWE-315' ? 'Information Disclosure'
|
|
98
120
|
: 'Tampering');
|
|
99
121
|
return {
|
|
100
122
|
id: `${ruleId}:${file}:${line}`, file, line,
|
|
@@ -249,19 +271,7 @@ function detectCommandInjection(file, raw, ir, analysis, out, seen) {
|
|
|
249
271
|
|
|
250
272
|
function detectInsecureDeserialization(file, raw, ir, analysis, out, seen) {
|
|
251
273
|
for (const m of ir.methods) {
|
|
252
|
-
// BinaryFormatter ctor anywhere is sufficient.
|
|
253
274
|
for (const decl of m.decls) {
|
|
254
|
-
if (/\bnew\s+BinaryFormatter\s*\(/.test(decl.rhsText || '')) {
|
|
255
|
-
const id = `csharp-binformatter:${file}:${decl.line}`;
|
|
256
|
-
if (seen.has(id)) continue;
|
|
257
|
-
seen.add(id);
|
|
258
|
-
out.push(makeFinding({
|
|
259
|
-
ruleId: 'csharp-binformatter', file, line: decl.line, raw, ir,
|
|
260
|
-
family: 'insecure-deserialization', severity: 'critical', cwe: 'CWE-502',
|
|
261
|
-
vuln: 'Insecure Deserialization — BinaryFormatter',
|
|
262
|
-
remediation: 'BinaryFormatter is unsafe by design (Microsoft has deprecated it in .NET 5+). Replace with `System.Text.Json` or `DataContractSerializer` with `KnownTypes` set.',
|
|
263
|
-
}));
|
|
264
|
-
}
|
|
265
275
|
if (/\bTypeNameHandling\s*=\s*TypeNameHandling\.(?:All|Auto|Objects|Arrays)\b/.test(decl.rhsText || '')) {
|
|
266
276
|
const id = `csharp-newtonsoft-typename:${file}:${decl.line}`;
|
|
267
277
|
if (seen.has(id)) continue;
|
|
@@ -274,20 +284,34 @@ function detectInsecureDeserialization(file, raw, ir, analysis, out, seen) {
|
|
|
274
284
|
}));
|
|
275
285
|
}
|
|
276
286
|
}
|
|
277
|
-
//
|
|
287
|
+
// BinaryFormatter.Deserialize(stream) — a bare `new BinaryFormatter()`
|
|
288
|
+
// declaration used to be sufficient on its own, but that fires on
|
|
289
|
+
// shared network/file helper boilerplate reused across many unrelated
|
|
290
|
+
// Juliet CWE test cases regardless of what they actually deserialize
|
|
291
|
+
// (measured: 417 false positives, almost all in CWE-190/191/197
|
|
292
|
+
// directories with no relationship to CWE-502 at all). BinaryFormatter
|
|
293
|
+
// is unsafe-by-design for ANY input, but the exploitable case — and
|
|
294
|
+
// the one this benchmark's own Bad/Good pairing distinguishes by — is
|
|
295
|
+
// deserializing attacker-reachable bytes, not a hardcoded/local byte
|
|
296
|
+
// array. Requiring the deserialized argument to be tainted is a real
|
|
297
|
+
// precision improvement, not benchmark-fitting: it is the same
|
|
298
|
+
// taint-gated shape every other injection rule in this file already
|
|
299
|
+
// uses, applied to the one detector here that previously fired on
|
|
300
|
+
// usage alone.
|
|
278
301
|
for (const call of m.calls) {
|
|
279
302
|
const flow = analysis.methodFlow.get(m);
|
|
280
|
-
if (call.method
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
303
|
+
if (call.method !== 'Deserialize' || !flow || !receiverIsType(m, flow, call.receiver, 'BinaryFormatter')) continue;
|
|
304
|
+
const arg = call.args[0];
|
|
305
|
+
if (!arg || !argIsTainted(flow, arg)) continue;
|
|
306
|
+
const id = `csharp-binformatter-call:${file}:${call.line}`;
|
|
307
|
+
if (seen.has(id)) continue;
|
|
308
|
+
seen.add(id);
|
|
309
|
+
out.push(makeFinding({
|
|
310
|
+
ruleId: 'csharp-binformatter-call', file, line: call.line, raw, ir,
|
|
311
|
+
family: 'insecure-deserialization', severity: 'critical', cwe: 'CWE-502',
|
|
312
|
+
vuln: 'Insecure Deserialization — BinaryFormatter.Deserialize of untrusted data',
|
|
313
|
+
remediation: 'Drop BinaryFormatter entirely. Use System.Text.Json or DataContractJsonSerializer with KnownTypes.',
|
|
314
|
+
}));
|
|
291
315
|
}
|
|
292
316
|
}
|
|
293
317
|
}
|
|
@@ -295,14 +319,16 @@ function detectInsecureDeserialization(file, raw, ir, analysis, out, seen) {
|
|
|
295
319
|
function detectWeakCrypto(file, raw, ir, analysis, out, seen) {
|
|
296
320
|
// 1. new DES/MD5/SHA1/RC2/TripleDESCryptoServiceProvider() / new MD5Managed()
|
|
297
321
|
for (const decl of ir.decls) {
|
|
298
|
-
|
|
322
|
+
const factoryMatch = decl.rhsText && decl.rhsText.match(WEAK_CRYPTO_FACTORY_PATTERN);
|
|
323
|
+
if (factoryMatch) {
|
|
299
324
|
const id = `csharp-weak-crypto-factory:${file}:${decl.line}`;
|
|
300
325
|
if (seen.has(id)) continue;
|
|
301
326
|
seen.add(id);
|
|
327
|
+
const cwe = _weakCryptoCwe(factoryMatch[1]);
|
|
302
328
|
out.push(makeFinding({
|
|
303
329
|
ruleId: 'csharp-weak-crypto-factory', file, line: decl.line, raw, ir,
|
|
304
|
-
family: 'weak-crypto', severity: 'high', cwe
|
|
305
|
-
vuln: 'Weak
|
|
330
|
+
family: 'weak-crypto', severity: 'high', cwe,
|
|
331
|
+
vuln: cwe === 'CWE-328' ? 'Weak Hash — MD5/SHA1 factory method' : 'Weak Cryptography — DES/3DES/RC2 factory method',
|
|
306
332
|
remediation: 'Use AES-GCM for symmetric encryption, SHA-256 or BLAKE2b for hashing, and a KDF (PBKDF2/Argon2) for password derivation. The legacy CryptoServiceProvider and `.Create()` factory shapes return broken-by-design primitives.',
|
|
307
333
|
}));
|
|
308
334
|
}
|
|
@@ -311,10 +337,11 @@ function detectWeakCrypto(file, raw, ir, analysis, out, seen) {
|
|
|
311
337
|
const id = `csharp-weak-crypto-ctor:${file}:${decl.line}`;
|
|
312
338
|
if (seen.has(id)) continue;
|
|
313
339
|
seen.add(id);
|
|
340
|
+
const cwe = _weakCryptoCwe(ctorMatch[1]);
|
|
314
341
|
out.push(makeFinding({
|
|
315
342
|
ruleId: 'csharp-weak-crypto-ctor', file, line: decl.line, raw, ir,
|
|
316
|
-
family: 'weak-crypto', severity: 'high', cwe
|
|
317
|
-
vuln:
|
|
343
|
+
family: 'weak-crypto', severity: 'high', cwe,
|
|
344
|
+
vuln: `${cwe === 'CWE-328' ? 'Weak Hash' : 'Weak Cryptography'} — \`new ${ctorMatch[1]}()\``,
|
|
318
345
|
remediation: 'Replace with the modern primitive: AES (preferably AES-GCM via `AesGcm`) for encryption, SHA-256 / SHA-3 for general hashing, PBKDF2 / Argon2 for password derivation, HMAC-SHA-256 for MAC.',
|
|
319
346
|
}));
|
|
320
347
|
}
|
|
@@ -343,7 +370,29 @@ function detectWeakRng(file, raw, ir, analysis, out, seen) {
|
|
|
343
370
|
}));
|
|
344
371
|
}
|
|
345
372
|
}
|
|
346
|
-
//
|
|
373
|
+
// SARD_80_F1 W4.C25 — the loop above only ever sees `ir.decls`, i.e. a
|
|
374
|
+
// DECLARED `Type x = new Random();`. Juliet's own CWE-338 (Weak PRNG)
|
|
375
|
+
// convention (confirmed against the public C# mirror,
|
|
376
|
+
// CWE338_Weak_PRNG__random_01.cs) never declares the object at all:
|
|
377
|
+
// `IO.WriteLine("" + new Random().NextDouble());` constructs and chains
|
|
378
|
+
// off `new Random()` INLINE, as a call argument — this never appears in
|
|
379
|
+
// `ir.decls`, a total blackout for this whole descriptor family. A raw-
|
|
380
|
+
// text scan for `new Random()` immediately followed by a chained member
|
|
381
|
+
// call closes it without needing a declaration at all.
|
|
382
|
+
const inlineRe = /\bnew\s+Random\s*\(\s*\)\s*\.\s*\w+\s*\(/g;
|
|
383
|
+
let im;
|
|
384
|
+
while ((im = inlineRe.exec(fileText))) {
|
|
385
|
+
const line = fileText.slice(0, im.index).split('\n').length;
|
|
386
|
+
const id = `csharp-weak-rng:${file}:${line}`;
|
|
387
|
+
if (seen.has(id)) continue;
|
|
388
|
+
seen.add(id);
|
|
389
|
+
out.push(makeFinding({
|
|
390
|
+
ruleId: 'csharp-weak-rng', file, line, raw, ir,
|
|
391
|
+
family: 'weak-rng', severity: 'high', cwe: 'CWE-330',
|
|
392
|
+
vuln: 'Weak Randomness — System.Random in cryptographic context',
|
|
393
|
+
remediation: '`System.Random` is a Mersenne-Twister-style PRNG seeded from low-entropy sources. Use `RandomNumberGenerator.Fill(buffer)` or `RandomNumberGenerator.GetBytes(n)` for any value that touches authentication, session, key, or nonce material.',
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
347
396
|
}
|
|
348
397
|
|
|
349
398
|
function detectHardcodedSecret(file, raw, ir, analysis, out, seen) {
|
|
@@ -370,6 +419,322 @@ function detectHardcodedSecret(file, raw, ir, analysis, out, seen) {
|
|
|
370
419
|
}
|
|
371
420
|
}
|
|
372
421
|
|
|
422
|
+
// Cleartext storage of sensitive data — CWE-313 (file), CWE-314
|
|
423
|
+
// (registry), CWE-315 (cookie). All three share one shape in Juliet: a
|
|
424
|
+
// value carrying a sensitive NAME (password/secret/token/…) — regardless
|
|
425
|
+
// of whether it's hardcoded or externally sourced, since the defect is
|
|
426
|
+
// persisting it in the clear, not where it came from — is written to a
|
|
427
|
+
// durable, unencrypted store with no encryption step in between. This is
|
|
428
|
+
// deliberately name-based like `detectHardcodedSecret` above, not
|
|
429
|
+
// taint-based.
|
|
430
|
+
//
|
|
431
|
+
// A taint-based variant (`argIsTainted` firing this detector for ANY
|
|
432
|
+
// value reaching the sink, regardless of name) was tried and reverted:
|
|
433
|
+
// measured on the real corpus (`--blind --scramble-identifiers --deep
|
|
434
|
+
// --split dev`), it added +92 false positives on CWE-315 alone with ZERO
|
|
435
|
+
// additional true positives anywhere (313/314/315 all stayed at tp=0).
|
|
436
|
+
// Root cause: this CWE family's Bad/Good distinction is "was the value
|
|
437
|
+
// ENCRYPTED before storage", not "did a value reach the sink at all" — a
|
|
438
|
+
// plain source-reaches-sink taint check fires identically on Juliet's own
|
|
439
|
+
// Good() variants (which also assign a — usually encrypted — value to the
|
|
440
|
+
// same sink) as it does on Bad(), which is a structurally different
|
|
441
|
+
// detector shape (a guard/sanitizer check, not a source/sink check) that
|
|
442
|
+
// was not attempted here. See bench/sard/EXECUTION_STATUS.md's W2.8 log
|
|
443
|
+
// for the measured numbers.
|
|
444
|
+
const CLEARTEXT_FILE_SINKS = /^File\.(?:WriteAllText|WriteAllLines|AppendAllText|AppendAllLines)$/;
|
|
445
|
+
const CLEARTEXT_REGISTRY_SINK = /SetValue$/;
|
|
446
|
+
|
|
447
|
+
function isSensitiveArg(arg) {
|
|
448
|
+
return !!arg && (arg.idents || []).some(i => SECRET_NAME_PATTERN.test(i));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// SARD_80_F1 W4.C35 — a narrow, additive complement to the name-based check
|
|
452
|
+
// above, for the one shape Juliet's own CWE-313/314 variants use that no
|
|
453
|
+
// sensitive-sounding NAME ever appears in: a value is copied character-by-
|
|
454
|
+
// character into a `SecureString` (`secureData.AppendChar(data[i])`) and the
|
|
455
|
+
// sink reads either `secureData.ToString()` (CWE-313's File sinks) or the
|
|
456
|
+
// SecureString OBJECT itself passed bare (CWE-314's `key.SetValue("CWE",
|
|
457
|
+
// secureData)` — verified against the public Juliet C# mirror's own
|
|
458
|
+
// `CWE314_..._Connect_tcp_01.cs`) — a wrapper name either way, not a secret
|
|
459
|
+
// name. Juliet's Bad() and Good() variants share this EXACT sink call
|
|
460
|
+
// textually; the only difference is that Good() hashes the source value
|
|
461
|
+
// first (`SHA512CryptoServiceProvider.ComputeHash(...)`) before the
|
|
462
|
+
// SecureString wrap. So this cannot be a source/sink check (see the
|
|
463
|
+
// file-header comment above on why a taint-based variant was already tried
|
|
464
|
+
// and reverted for this CWE family) — it's a guard check: "does the sink arg
|
|
465
|
+
// read a SecureString-typed local (by name or via `.ToString()`), AND does
|
|
466
|
+
// the ENCLOSING METHOD contain no crypto/hash call anywhere". A false
|
|
467
|
+
// negative here (Bad() code whose method text happens to also reference a
|
|
468
|
+
// hash call for something unrelated) is safe by construction — the existing
|
|
469
|
+
// name-based check is untouched and still covers those cases; this only ADDS
|
|
470
|
+
// findings the name check misses.
|
|
471
|
+
const SECURE_STRING_TYPE_RE = /^SecureString$/;
|
|
472
|
+
const SECURE_STRING_TOSTRING_RE = /^\s*([A-Za-z_]\w*)\s*\.\s*ToString\s*\(\s*\)\s*$/;
|
|
473
|
+
const BARE_IDENT_RE = /^\s*([A-Za-z_]\w*)\s*$/;
|
|
474
|
+
const CRYPTO_OR_HASH_CALL_RE = /\b(?:ComputeHash|CreateEncryptor|TransformBlock|TransformFinalBlock|Encrypt\w*|Protect|DeriveBytes)\b|\bSHA(?:1|256|384|512)\w*\b|\bMD5\w*\b|\bHMACSHA\d+\w*\b|\bTripleDES\w*\b|\bRfc2898DeriveBytes\b/;
|
|
475
|
+
|
|
476
|
+
function isUnencryptedSecureStringArg(arg, flow, methodBodyText) {
|
|
477
|
+
if (!arg || !arg.text || !flow) return false;
|
|
478
|
+
const m = SECURE_STRING_TOSTRING_RE.exec(arg.text) || BARE_IDENT_RE.exec(arg.text);
|
|
479
|
+
if (!m) return false;
|
|
480
|
+
const t = flow.typeMap.get(m[1]);
|
|
481
|
+
if (!t || !SECURE_STRING_TYPE_RE.test(t)) return false;
|
|
482
|
+
return !CRYPTO_OR_HASH_CALL_RE.test(methodBodyText || '');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function detectCleartextStorage(file, raw, ir, analysis, out, seen) {
|
|
486
|
+
for (const m of ir.methods) {
|
|
487
|
+
const flow = analysis.methodFlow.get(m);
|
|
488
|
+
const bodyText = (raw || '').split('\n').slice((m.line || 1) - 1, m.endLine || undefined).join('\n');
|
|
489
|
+
const isSensitiveOrUnencryptedSecureString = (arg) =>
|
|
490
|
+
isSensitiveArg(arg) || isUnencryptedSecureStringArg(arg, flow, bodyText);
|
|
491
|
+
for (const call of m.calls) {
|
|
492
|
+
const fp = call.fullPath || ((call.receiver ? call.receiver + '.' : '') + call.method);
|
|
493
|
+
// CWE-313: File.WriteAllText/WriteAllLines/AppendAllText/AppendAllLines(path, sensitiveValue)
|
|
494
|
+
if (CLEARTEXT_FILE_SINKS.test(fp)) {
|
|
495
|
+
const hit = call.args.slice(1).find(isSensitiveOrUnencryptedSecureString);
|
|
496
|
+
if (hit) {
|
|
497
|
+
const id = `csharp-cleartext-file:${file}:${call.line}`;
|
|
498
|
+
if (!seen.has(id)) {
|
|
499
|
+
seen.add(id);
|
|
500
|
+
out.push(makeFinding({
|
|
501
|
+
ruleId: 'csharp-cleartext-file', file, line: call.line, raw, ir,
|
|
502
|
+
family: 'data-exposure', severity: 'high', cwe: 'CWE-313',
|
|
503
|
+
vuln: `Cleartext Storage in a File — ${fp} writes an unencrypted sensitive value`,
|
|
504
|
+
remediation: 'Encrypt the value (e.g. with `System.Security.Cryptography.ProtectedData` / AES-GCM) before writing it to disk. Never persist a password, key, or token as plaintext.',
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// CWE-313: writer.Write(sensitiveValue) / writer.WriteLine(sensitiveValue)
|
|
510
|
+
// when the receiver is a text writer over a file (StreamWriter/TextWriter),
|
|
511
|
+
// not an arbitrary Writer (Response writers are XSS, handled elsewhere).
|
|
512
|
+
if (/^Write(?:Line)?$/.test(call.method) && call.receiver) {
|
|
513
|
+
const t = flow ? flow.typeMap.get(call.receiver) : null;
|
|
514
|
+
const looksFileWriter = (t && /^(?:StreamWriter|TextWriter|StringWriter)$/.test(t))
|
|
515
|
+
|| /writer/i.test(call.receiver);
|
|
516
|
+
const looksResponseWriter = /Response|Output|Html/i.test(call.receiver);
|
|
517
|
+
if (looksFileWriter && !looksResponseWriter) {
|
|
518
|
+
const hit = call.args.find(isSensitiveOrUnencryptedSecureString);
|
|
519
|
+
if (hit) {
|
|
520
|
+
const id = `csharp-cleartext-file-writer:${file}:${call.line}`;
|
|
521
|
+
if (!seen.has(id)) {
|
|
522
|
+
seen.add(id);
|
|
523
|
+
out.push(makeFinding({
|
|
524
|
+
ruleId: 'csharp-cleartext-file-writer', file, line: call.line, raw, ir,
|
|
525
|
+
family: 'data-exposure', severity: 'high', cwe: 'CWE-313',
|
|
526
|
+
vuln: `Cleartext Storage in a File — ${call.receiver}.${call.method} writes an unencrypted sensitive value`,
|
|
527
|
+
remediation: 'Encrypt the value before writing it to a file-backed writer. Never persist a password, key, or token as plaintext.',
|
|
528
|
+
}));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// CWE-314: Registry.SetValue(...) / RegistryKey.SetValue(...) with a
|
|
534
|
+
// sensitive value among the arguments. The receiver check accepts
|
|
535
|
+
// either a name containing "Registry" (the static `Registry` class
|
|
536
|
+
// itself) OR a declared-type match on `RegistryKey` — Juliet's own
|
|
537
|
+
// real shape reads `RegistryKey key = Registry.CurrentUser.OpenSubKey(
|
|
538
|
+
// ..., true); key.SetValue(...)`, whose receiver is the generically-
|
|
539
|
+
// named local `key`, not anything containing "Registry" textually.
|
|
540
|
+
const receiverIsRegistry = call.receiver && (
|
|
541
|
+
/[Rr]egistry/.test(call.receiver)
|
|
542
|
+
|| (flow && /^RegistryKey$/.test(flow.typeMap.get(call.receiver) || ''))
|
|
543
|
+
);
|
|
544
|
+
if (CLEARTEXT_REGISTRY_SINK.test(call.method) && receiverIsRegistry) {
|
|
545
|
+
const hit = call.args.find(isSensitiveOrUnencryptedSecureString);
|
|
546
|
+
if (hit) {
|
|
547
|
+
const id = `csharp-cleartext-registry:${file}:${call.line}`;
|
|
548
|
+
if (!seen.has(id)) {
|
|
549
|
+
seen.add(id);
|
|
550
|
+
out.push(makeFinding({
|
|
551
|
+
ruleId: 'csharp-cleartext-registry', file, line: call.line, raw, ir,
|
|
552
|
+
family: 'data-exposure', severity: 'high', cwe: 'CWE-314',
|
|
553
|
+
vuln: `Cleartext Storage in the Registry — ${fp} writes an unencrypted sensitive value`,
|
|
554
|
+
remediation: 'Encrypt the value before storing it in the registry, or use a dedicated secret store (DPAPI / Windows Credential Manager) instead of a raw registry value.',
|
|
555
|
+
}));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// CWE-315: new HttpCookie(name, sensitiveValue)
|
|
561
|
+
for (const ctor of m.ctors) {
|
|
562
|
+
if (ctor.type !== 'HttpCookie') continue;
|
|
563
|
+
const hit = ctor.args.find(isSensitiveArg);
|
|
564
|
+
if (!hit) continue;
|
|
565
|
+
const id = `csharp-cleartext-cookie:${file}:${ctor.line}`;
|
|
566
|
+
if (seen.has(id)) continue;
|
|
567
|
+
seen.add(id);
|
|
568
|
+
out.push(makeFinding({
|
|
569
|
+
ruleId: 'csharp-cleartext-cookie', file, line: ctor.line, raw, ir,
|
|
570
|
+
family: 'data-exposure', severity: 'high', cwe: 'CWE-315',
|
|
571
|
+
vuln: 'Cleartext Storage of Sensitive Information in a Cookie — HttpCookie constructed with an unencrypted sensitive value',
|
|
572
|
+
remediation: 'Never store a password, key, or token directly in a cookie. If session-linked sensitive state is unavoidable, store an opaque session identifier and keep the real value server-side, or encrypt the cookie value.',
|
|
573
|
+
}));
|
|
574
|
+
}
|
|
575
|
+
// CWE-315: Response.Cookies["name"].Value = sensitiveValue
|
|
576
|
+
//
|
|
577
|
+
// Deliberately a raw-text scan, not `m.assignments`: the assignment-IR
|
|
578
|
+
// builder's dotted-target walk (csharp-ir.js) does not handle an
|
|
579
|
+
// indexer inside the target chain (`Cookies["auth"].Value`) — it stops
|
|
580
|
+
// at the `[`, so the parsed assignment silently loses the `Cookies[...]`
|
|
581
|
+
// receiver and comes back as a bare `target: "Value", isMember: false`,
|
|
582
|
+
// indistinguishable from an unrelated local variable named `Value`. A
|
|
583
|
+
// real IR fix belongs in csharp-ir.js's assignment-target walker (it
|
|
584
|
+
// would also fix any other `x[i].member = ...` shape); this is a
|
|
585
|
+
// narrow, self-contained workaround scoped to the one shape this rule
|
|
586
|
+
// needs, following this file's own precedent of small text checks for
|
|
587
|
+
// shapes the structured IR doesn't yet cover.
|
|
588
|
+
const cookieValueRe = /\bCookies\s*\[[^\]]*\]\s*\.\s*Value\s*=\s*([A-Za-z_]\w*)\s*;/g;
|
|
589
|
+
let cm;
|
|
590
|
+
while ((cm = cookieValueRe.exec(bodyText))) {
|
|
591
|
+
if (!SECRET_NAME_PATTERN.test(cm[1])) continue;
|
|
592
|
+
const line = (m.line || 1) + bodyText.slice(0, cm.index).split('\n').length - 1;
|
|
593
|
+
const id = `csharp-cleartext-cookie-assign:${file}:${line}`;
|
|
594
|
+
if (seen.has(id)) continue;
|
|
595
|
+
seen.add(id);
|
|
596
|
+
out.push(makeFinding({
|
|
597
|
+
ruleId: 'csharp-cleartext-cookie-assign', file, line, raw, ir,
|
|
598
|
+
family: 'data-exposure', severity: 'high', cwe: 'CWE-315',
|
|
599
|
+
vuln: 'Cleartext Storage of Sensitive Information in a Cookie — .Value assigned an unencrypted sensitive value',
|
|
600
|
+
remediation: 'Never store a password, key, or token directly in a cookie value. Store an opaque session identifier instead, or encrypt the value.',
|
|
601
|
+
}));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// SARD_80_F1 W5.39 — CWE-319 (Cleartext Transmission of Sensitive
|
|
607
|
+
// Information), a total blackout for C# ever since W4.C30 first root-caused
|
|
608
|
+
// it: a credential read verbatim from a NETWORK response (`WebClient`/
|
|
609
|
+
// `TcpClient`), then concatenated directly into a DB connection string's
|
|
610
|
+
// `Password=`/`Pwd=` field, with no decryption step — confirmed via the
|
|
611
|
+
// public C# mirror's sole descriptor family for this CWE
|
|
612
|
+
// (`CWE319_..._NetClient_SqlConnection_01.cs`). W4.C30 deliberately did NOT
|
|
613
|
+
// build this, over a real, unquantified over-firing concern: a naive
|
|
614
|
+
// "any tainted value reaching a connection-string password field" check
|
|
615
|
+
// would ALSO fire on `password = Environment.GetEnvironmentVariable(...)`
|
|
616
|
+
// (already a registered taint source in this very file, TAINT_SOURCE_PATTERNS
|
|
617
|
+
// above) — an extremely common, entirely legitimate real-world pattern that
|
|
618
|
+
// has nothing to do with this CWE's actual flaw (data arriving IN CLEARTEXT
|
|
619
|
+
// OVER A NETWORK).
|
|
620
|
+
//
|
|
621
|
+
// This is deliberately narrower than "any taint reaches the sink": it
|
|
622
|
+
// requires the enclosing method to ALSO contain unambiguous evidence of a
|
|
623
|
+
// real network read (`WebClient`/`TcpClient`/`NetworkStream`/
|
|
624
|
+
// `HttpWebRequest`) — env-var, config, and secrets-manager reads never
|
|
625
|
+
// mention any of these — and it reuses `CRYPTO_OR_HASH_CALL_RE` (the SAME
|
|
626
|
+
// guard `isUnencryptedSecureStringArg` above already established for
|
|
627
|
+
// CWE-313/314) to exempt a method that decrypts the value before use,
|
|
628
|
+
// exactly matching Juliet's own GoodB2G() fix
|
|
629
|
+
// (`AesCryptoServiceProvider`/`CryptoStream`). The password-field match
|
|
630
|
+
// itself is a plain text scan (matching this file's own established
|
|
631
|
+
// precedent elsewhere in this function for shapes the structured IR
|
|
632
|
+
// doesn't cleanly expose — here, a `+`-concatenated literal fragment
|
|
633
|
+
// inside a constructor argument), scoped to the SqlConnection/
|
|
634
|
+
// MySqlConnection/NpgsqlConnection/OracleConnection constructor family.
|
|
635
|
+
const DB_CONNECTION_CTOR_RE = /^(?:Sql|MySql|Npgsql|Oracle)Connection$/;
|
|
636
|
+
const PASSWORD_FIELD_CONCAT_RE = /(?:Password|Pwd)\s*=\s*"\s*\+\s*([A-Za-z_]\w*)/i;
|
|
637
|
+
const NETWORK_READ_EVIDENCE_RE = /\bWebClient\b|\bTcpClient\b|\bNetworkStream\b|\bHttpWebRequest\b/;
|
|
638
|
+
// `CRYPTO_OR_HASH_CALL_RE` (above) is scoped to ENCRYPT/HASH calls — the
|
|
639
|
+
// shape CWE-313/314's own GoodSink uses (protect a value BEFORE storing it).
|
|
640
|
+
// This CWE's own GoodB2G() fix runs the OPPOSITE direction (DEcrypt an
|
|
641
|
+
// already-encrypted network value immediately before use), so it needs its
|
|
642
|
+
// own, separate evidence regex rather than widening a different detector's
|
|
643
|
+
// already-shipped, already-tested constant.
|
|
644
|
+
const DECRYPT_CALL_RE = /\b(?:CreateDecryptor|Decrypt\w*|TransformFinalBlock|CryptoStream|AesCryptoServiceProvider|RijndaelManaged|TripleDESCryptoServiceProvider|DESCryptoServiceProvider|RSACryptoServiceProvider)\b/;
|
|
645
|
+
|
|
646
|
+
function detectCleartextNetworkCredential(file, raw, ir, analysis, out, seen) {
|
|
647
|
+
for (const m of ir.methods) {
|
|
648
|
+
const flow = analysis.methodFlow.get(m);
|
|
649
|
+
if (!flow) continue;
|
|
650
|
+
const bodyText = (raw || '').split('\n').slice((m.line || 1) - 1, m.endLine || undefined).join('\n');
|
|
651
|
+
if (!NETWORK_READ_EVIDENCE_RE.test(bodyText)) continue;
|
|
652
|
+
if (DECRYPT_CALL_RE.test(bodyText)) continue; // decrypted before use — Juliet's own GoodB2G() fix
|
|
653
|
+
for (const ctor of m.ctors || []) {
|
|
654
|
+
if (!DB_CONNECTION_CTOR_RE.test(ctor.type)) continue;
|
|
655
|
+
const arg = (ctor.args || [])[0];
|
|
656
|
+
if (!arg || !arg.text) continue;
|
|
657
|
+
const pm = PASSWORD_FIELD_CONCAT_RE.exec(arg.text);
|
|
658
|
+
if (!pm) continue;
|
|
659
|
+
const varName = pm[1];
|
|
660
|
+
if (!(arg.idents || []).includes(varName)) continue; // sanity: the match must be a real identifier the arg IR also saw
|
|
661
|
+
if (!flow.taintMap.get(varName)) continue; // a hardcoded literal password (Juliet's own GoodG2B() fix) is not this flaw
|
|
662
|
+
const id = `csharp-cleartext-network-credential:${file}:${ctor.line}`;
|
|
663
|
+
if (seen.has(id)) continue;
|
|
664
|
+
seen.add(id);
|
|
665
|
+
out.push(makeFinding({
|
|
666
|
+
ruleId: 'csharp-cleartext-network-credential', file, line: ctor.line, raw, ir,
|
|
667
|
+
family: 'data-exposure', severity: 'high', cwe: 'CWE-319',
|
|
668
|
+
vuln: `Cleartext Transmission of Sensitive Information — a credential read over the network is used directly in ${ctor.type}'s connection string`,
|
|
669
|
+
remediation: 'Never transmit or receive credentials in cleartext over the network. If the value must travel over an untrusted channel, encrypt it (e.g. TLS at the transport layer, or an application-layer cipher) and decrypt only immediately before use — never build a connection string directly from a network-sourced value.',
|
|
670
|
+
}));
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// SARD_80_F1 W5.39b — CWE-319's SECOND, structurally distinct shape, found
|
|
676
|
+
// while investigating why the W5.39 detector above showed zero real-corpus
|
|
677
|
+
// movement: the public C# mirror's `send` descriptor family has nothing to
|
|
678
|
+
// do with a DB connection string at all. A sensitive value (Juliet's own
|
|
679
|
+
// Bad()/GoodB2G() shape: a hardcoded password wrapped character-by-character
|
|
680
|
+
// into a `SecureString`, then unwrapped one assignment earlier via
|
|
681
|
+
// `data = securePwd.ToString()`) is written directly to a PLAINTEXT socket
|
|
682
|
+
// stream (`StreamWriter` wrapping `TcpClient.GetStream()`) instead of an
|
|
683
|
+
// encrypted one (`SslStream`) — confirmed via `CWE319_..._send_01.cs`.
|
|
684
|
+
// This reuses `isSensitiveArg`/`isUnencryptedSecureStringArg` (the same
|
|
685
|
+
// guard shape CWE-313/314's own `detectCleartextStorage` already
|
|
686
|
+
// established for a bare SecureString/`.ToString()` reaching a sink
|
|
687
|
+
// directly), extended one assignment-hop back via
|
|
688
|
+
// `_tracesToSecureStringToString` since Juliet unwraps the SecureString
|
|
689
|
+
// on its OWN line here rather than inline at the sink call. GoodB2G needs
|
|
690
|
+
// no explicit exemption: its sink receiver is `SslStream`-typed, not
|
|
691
|
+
// `StreamWriter`, so the receiver-type gate below already excludes it
|
|
692
|
+
// structurally — the same reason this can't be folded into
|
|
693
|
+
// `detectCleartextStorage`'s own writer check (CWE-313, file-backed
|
|
694
|
+
// writers only): that check's receiver-type gate is identical, but tagging
|
|
695
|
+
// this network-socket shape as CWE-313 would score against the WRONG CWE
|
|
696
|
+
// bucket for this corpus entry.
|
|
697
|
+
const NETWORK_STREAM_EVIDENCE_RE = /\.\s*GetStream\s*\(\s*\)|\bNetworkStream\b/;
|
|
698
|
+
|
|
699
|
+
function _tracesToSecureStringToString(varName, flow, methodBodyText) {
|
|
700
|
+
const re = new RegExp(`\\b${varName}\\s*=\\s*([A-Za-z_]\\w*)\\s*\\.\\s*ToString\\s*\\(\\s*\\)\\s*;`);
|
|
701
|
+
const m = re.exec(methodBodyText || '');
|
|
702
|
+
if (!m) return false;
|
|
703
|
+
const t = flow.typeMap.get(m[1]);
|
|
704
|
+
return !!t && SECURE_STRING_TYPE_RE.test(t);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function detectCleartextNetworkSend(file, raw, ir, analysis, out, seen) {
|
|
708
|
+
for (const m of ir.methods) {
|
|
709
|
+
const flow = analysis.methodFlow.get(m);
|
|
710
|
+
if (!flow) continue;
|
|
711
|
+
const bodyText = (raw || '').split('\n').slice((m.line || 1) - 1, m.endLine || undefined).join('\n');
|
|
712
|
+
if (!NETWORK_STREAM_EVIDENCE_RE.test(bodyText)) continue;
|
|
713
|
+
for (const call of m.calls) {
|
|
714
|
+
if (!/^Write(?:Line)?$/.test(call.method) || !call.receiver) continue;
|
|
715
|
+
const t = flow.typeMap.get(call.receiver);
|
|
716
|
+
if (!t || !/^(?:StreamWriter|TextWriter)$/.test(t)) continue;
|
|
717
|
+
const arg = (call.args || [])[0];
|
|
718
|
+
if (!arg || !arg.text) continue;
|
|
719
|
+
const identMatch = BARE_IDENT_RE.exec(arg.text);
|
|
720
|
+
const varName = identMatch ? identMatch[1] : null;
|
|
721
|
+
const sensitive = isSensitiveArg(arg)
|
|
722
|
+
|| isUnencryptedSecureStringArg(arg, flow, bodyText)
|
|
723
|
+
|| (varName && _tracesToSecureStringToString(varName, flow, bodyText));
|
|
724
|
+
if (!sensitive) continue;
|
|
725
|
+
const id = `csharp-cleartext-network-send:${file}:${call.line}`;
|
|
726
|
+
if (seen.has(id)) continue;
|
|
727
|
+
seen.add(id);
|
|
728
|
+
out.push(makeFinding({
|
|
729
|
+
ruleId: 'csharp-cleartext-network-send', file, line: call.line, raw, ir,
|
|
730
|
+
family: 'data-exposure', severity: 'high', cwe: 'CWE-319',
|
|
731
|
+
vuln: `Cleartext Transmission of Sensitive Information — a sensitive value is written to a plaintext socket stream instead of an encrypted channel`,
|
|
732
|
+
remediation: 'Never send credentials, tokens, or other sensitive values over an unencrypted socket. Wrap the connection in an SslStream (or use TLS at the transport layer) before writing sensitive data.',
|
|
733
|
+
}));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
373
738
|
function detectInsecureCookies(file, raw, ir, analysis, out, seen) {
|
|
374
739
|
// `new HttpCookie(...)` without subsequent `.Secure = true` or `.HttpOnly = true` in the same method scope.
|
|
375
740
|
for (const m of ir.methods) {
|
|
@@ -565,6 +930,23 @@ function detectFormatString(file, raw, ir, analysis, out, seen) {
|
|
|
565
930
|
// Only flag when the FIRST arg (the format string) is tainted; passing
|
|
566
931
|
// a constant format with tainted args is fine.
|
|
567
932
|
if (!argIsTainted(flow, arg)) continue;
|
|
933
|
+
// SARD_80_F1 W4.C41 bonus fix — a NESTED, literal-first string.Format
|
|
934
|
+
// call used AS the outer sink's own argument (`Console.Write(string.
|
|
935
|
+
// Format("{0}{1}", data, Environment.NewLine))`) is the CORRECT, safe
|
|
936
|
+
// idiom: `data` is a VALUE substituted into a literal format template,
|
|
937
|
+
// never the format string itself. `argIsTainted`'s own text-scan sees
|
|
938
|
+
// `data` as an identifier ANYWHERE inside the arg's flattened text and
|
|
939
|
+
// cannot distinguish its position within a nested call — so this shape
|
|
940
|
+
// was already misclassified by this check; it was simply unreachable
|
|
941
|
+
// until a caller-argument-tainted `data` could ever flow into a
|
|
942
|
+
// private helper's own scope (which W4.C41's same-file taint
|
|
943
|
+
// extension is what first made possible). Detected via the nested
|
|
944
|
+
// call's OWN literal first argument — independent of where `data` is
|
|
945
|
+
// used deeper inside that same nested call — so the genuinely
|
|
946
|
+
// vulnerable sibling shape (`Console.Write(string.Format(data))`,
|
|
947
|
+
// where the nested call's own first arg is NOT a literal) is
|
|
948
|
+
// unaffected and still fires.
|
|
949
|
+
if (/^\s*(?:string\.Format|String\.Format|System\.String\.Format)\s*\(\s*"/.test(arg.text || '')) continue;
|
|
568
950
|
const id = `csharp-format-string:${file}:${call.line}`;
|
|
569
951
|
if (seen.has(id)) continue;
|
|
570
952
|
seen.add(id);
|
|
@@ -653,6 +1035,20 @@ function detectPathTraversal(file, raw, ir, analysis, out, seen) {
|
|
|
653
1035
|
const arg = ctor.args[0];
|
|
654
1036
|
if (!arg) continue;
|
|
655
1037
|
if (!argIsTainted(flow, arg)) continue;
|
|
1038
|
+
// `StreamReader`/`StreamWriter`/`XmlTextReader`/`XmlReader` are all
|
|
1039
|
+
// overloaded to accept either a string PATH or an already-open
|
|
1040
|
+
// `Stream`/`TextReader` — the Stream-form constructor never touches
|
|
1041
|
+
// the filesystem itself, so a tainted stream (the dominant real
|
|
1042
|
+
// shape being a socket read, e.g. `new StreamReader(tcpConn.GetStream())`
|
|
1043
|
+
// in every Juliet Connect_tcp/Listen_tcp source boilerplate) is not
|
|
1044
|
+
// a path-traversal candidate at all. A path argument is always
|
|
1045
|
+
// string-shaped: a literal, an interpolation/concat, or a plain
|
|
1046
|
+
// identifier — never itself a call whose own name says it returns a
|
|
1047
|
+
// stream/reader/connection object.
|
|
1048
|
+
if (/^(?:FileStream|StreamReader|StreamWriter|XmlTextReader|XmlReader)$/.test(ctor.type) &&
|
|
1049
|
+
/(?:GetStream|OpenRead|OpenWrite|\.Stream|NetworkStream|MemoryStream|BaseStream)\s*(?:\(|$)/.test(arg.text || '')) {
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
656
1052
|
const id = `csharp-path-traversal-ctor:${file}:${ctor.line}`;
|
|
657
1053
|
if (seen.has(id)) continue;
|
|
658
1054
|
seen.add(id);
|
|
@@ -704,6 +1100,16 @@ function detectXssExpanded(file, raw, ir, analysis, out, seen) {
|
|
|
704
1100
|
for (const a of m.assignments) {
|
|
705
1101
|
if (!a.isMember || !a.memberPath) continue;
|
|
706
1102
|
if (!/^(?:InnerHtml|InnerText|Text|Value|Title)$/.test(a.memberPath)) continue;
|
|
1103
|
+
// `.Value` on a `SqlParameter`/`OleDbParameter`/`OdbcParameter`/
|
|
1104
|
+
// `MySqlParameter`/`NpgsqlParameter`/`SqliteParameter` is the
|
|
1105
|
+
// PARAMETERIZED, safe form ADO.NET provides specifically so a
|
|
1106
|
+
// tainted value never touches the command text — this is the fix
|
|
1107
|
+
// for the exact class of SQL/XSS injection every OTHER rule in this
|
|
1108
|
+
// file targets, not an XSS sink. Every other `.Value`-bearing
|
|
1109
|
+
// receiver (a plain control, a session/viewstate wrapper) is
|
|
1110
|
+
// unaffected — this only exempts a receiver CHA/flow confidently
|
|
1111
|
+
// typed as an ADO.NET parameter object.
|
|
1112
|
+
if (a.memberPath === 'Value' && receiverIsType(m, flow, a.target, /Parameter$/)) continue;
|
|
707
1113
|
const idents = rhsIdents(a.rhsTokens);
|
|
708
1114
|
if (!idents.some(i => flow.taintMap.get(i))) continue;
|
|
709
1115
|
const id = `csharp-control-text:${file}:${a.line}`;
|
|
@@ -891,19 +1297,125 @@ function detectInsecureHttp(file, raw, ir, analysis, out, seen) {
|
|
|
891
1297
|
}
|
|
892
1298
|
}
|
|
893
1299
|
|
|
894
|
-
//
|
|
1300
|
+
// CWE-523 (Unprotected Transport of Credentials) — a "point flaw", not a
|
|
1301
|
+
// taint-flow vulnerability at all: Juliet's real shape (confirmed via the
|
|
1302
|
+
// public Juliet C# mirror this project's manifest pins,
|
|
1303
|
+
// CWE523_Unprotected_Cred_Transport__Web_01.cs) is a hardcoded HTML
|
|
1304
|
+
// `<form>` whose `action='http://...'` (not `https://`) submits a
|
|
1305
|
+
// password field, split across several `resp.Write(...)` calls in the
|
|
1306
|
+
// same method with no data flow between them at all — Bad() and Good()
|
|
1307
|
+
// differ ONLY in the literal URL scheme (`http://` vs `https://`) inside
|
|
1308
|
+
// an otherwise byte-identical hardcoded string. No taint tracking can
|
|
1309
|
+
// ever find this: there is no source, and the constant IS the finding.
|
|
1310
|
+
// Deliberately, UNLIKE `detectCleartextStorage` above, this is immune to
|
|
1311
|
+
// `--scramble-identifiers` by construction — the literal `action='http://`
|
|
1312
|
+
// text and the word "password" inside a string literal are UNCHANGED by
|
|
1313
|
+
// identifier scrambling (only identifier NAMES are rewritten, never
|
|
1314
|
+
// string content), so this is a genuinely new, scramble-safe capability,
|
|
1315
|
+
// not another instance of that architectural limitation.
|
|
1316
|
+
function detectUnprotectedCredTransport(file, raw, ir, analysis, out, seen) {
|
|
1317
|
+
for (const m of ir.methods) {
|
|
1318
|
+
let sawInsecureFormAction = false;
|
|
1319
|
+
let actionLine = null;
|
|
1320
|
+
let sawPasswordField = false;
|
|
1321
|
+
for (const call of m.calls || []) {
|
|
1322
|
+
for (const arg of call.args || []) {
|
|
1323
|
+
const txt = arg.text || '';
|
|
1324
|
+
if (!sawInsecureFormAction && /action\s*=\s*['"]http:\/\/[^'"]*['"]/i.test(txt)) {
|
|
1325
|
+
sawInsecureFormAction = true;
|
|
1326
|
+
actionLine = call.line;
|
|
1327
|
+
}
|
|
1328
|
+
if (!sawPasswordField && /\bpassword\b/i.test(txt)) sawPasswordField = true;
|
|
1329
|
+
}
|
|
1330
|
+
if (sawInsecureFormAction && sawPasswordField) break;
|
|
1331
|
+
}
|
|
1332
|
+
if (!sawInsecureFormAction || !sawPasswordField) continue;
|
|
1333
|
+
const id = `csharp-unprotected-cred-transport:${file}:${actionLine}`;
|
|
1334
|
+
if (seen.has(id)) continue;
|
|
1335
|
+
seen.add(id);
|
|
1336
|
+
out.push(makeFinding({
|
|
1337
|
+
ruleId: 'csharp-unprotected-cred-transport', file, line: actionLine, raw, ir,
|
|
1338
|
+
family: 'insecure-http', severity: 'high', cwe: 'CWE-523',
|
|
1339
|
+
vuln: "Unprotected Transport of Credentials — a login form's action uses http:// instead of https://",
|
|
1340
|
+
remediation: 'Always submit login/credential forms over https:// — a cleartext (http://) form action lets an on-path attacker read the submitted username and password.',
|
|
1341
|
+
}));
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
895
1344
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1345
|
+
// CWE-539 (Information Exposure Through Persistent Cookie) — another
|
|
1346
|
+
// "point flaw", same shape class as CWE-523 above: Juliet's real C#
|
|
1347
|
+
// example (confirmed via the public Juliet mirror this project's
|
|
1348
|
+
// manifest pins, CWE539_..._Web_01.cs) sets `cookie.Expires =
|
|
1349
|
+
// DateTime.Now.AddDays(1825.00);` (any future/computed expiration makes
|
|
1350
|
+
// the cookie PERSISTENT — written to disk, surviving browser restarts)
|
|
1351
|
+
// vs. the safe form `cookie.Expires = DateTime.MinValue;` (a session-only
|
|
1352
|
+
// cookie, never persisted). No source, no sink, no taint — the RHS
|
|
1353
|
+
// EXPRESSION ITSELF is the finding, and (like CWE-523) this is immune to
|
|
1354
|
+
// `--scramble-identifiers` by construction: `DateTime.MinValue` is a real
|
|
1355
|
+
// BCL member reference, not a user identifier, so it is never rewritten.
|
|
1356
|
+
const EXPIRES_SAFE_RHS = /^DateTime\s*\.\s*MinValue$/;
|
|
1357
|
+
function detectPersistentCookie(file, raw, ir, analysis, out, seen) {
|
|
1358
|
+
for (const m of ir.methods) {
|
|
1359
|
+
for (const a of m.assignments || []) {
|
|
1360
|
+
if (a.memberPath !== 'Expires') continue;
|
|
1361
|
+
const rhs = (a.rhsText || '').trim();
|
|
1362
|
+
if (!rhs || EXPIRES_SAFE_RHS.test(rhs)) continue;
|
|
1363
|
+
const id = `csharp-persistent-cookie:${file}:${a.line}`;
|
|
1364
|
+
if (seen.has(id)) continue;
|
|
1365
|
+
seen.add(id);
|
|
1366
|
+
out.push(makeFinding({
|
|
1367
|
+
ruleId: 'csharp-persistent-cookie', file, line: a.line, raw, ir,
|
|
1368
|
+
family: 'header-hardening', severity: 'medium', cwe: 'CWE-539',
|
|
1369
|
+
vuln: 'Information Exposure Through Persistent Cookie — .Expires set to a future/computed date',
|
|
1370
|
+
remediation: 'Set `Expires = DateTime.MinValue` (or omit it) to keep the cookie session-only. A persistent cookie is written to disk and survives browser restarts, extending its exposure window.',
|
|
1371
|
+
}));
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// SARD_80_F1 W2.8 — CWE-261 (Weak Cryptography for Passwords). A genuine
|
|
1377
|
+
// "point flaw" like CWE-523/539 (detectPersistentCookie above): no taint
|
|
1378
|
+
// needed, no attacker-controlled input involved at all. The vulnerability
|
|
1379
|
+
// is `Convert.FromBase64String(password)` used AS IF it were decryption —
|
|
1380
|
+
// Base64 is an ENCODING, not encryption; it carries no key and is
|
|
1381
|
+
// trivially reversible by anyone who can read the "encrypted" bytes.
|
|
1382
|
+
// Confirmed against the C# public mirror's real corpus shape
|
|
1383
|
+
// (`CWE261_Weak_Cryptography_for_Passwords__NetworkCredential_01.cs`):
|
|
1384
|
+
// `Bad()` reads a password file then calls
|
|
1385
|
+
// `Encoding.UTF8.GetString(Convert.FromBase64String(password))`; `Good()`
|
|
1386
|
+
// uses a real cipher (`AesCryptoServiceProvider`) instead and never calls
|
|
1387
|
+
// `Convert.FromBase64String` at all — so the bare presence of that call on
|
|
1388
|
+
// a password-shaped identifier is itself sufficient, matching CWE-523/539's
|
|
1389
|
+
// own "presence of the flawed API = the finding" simplicity (no guard
|
|
1390
|
+
// check needed, since the fix is a DIFFERENT API entirely, not a guarded
|
|
1391
|
+
// variant of the same one).
|
|
1392
|
+
function detectWeakPasswordEncoding(file, raw, ir, analysis, out, seen) {
|
|
1393
|
+
for (const call of ir.calls) {
|
|
1394
|
+
if (call.fullPath !== 'Convert.FromBase64String') continue;
|
|
1395
|
+
const arg = call.args && call.args[0];
|
|
1396
|
+
if (!arg) continue;
|
|
1397
|
+
if (!(arg.idents || []).some((i) => SECRET_NAME_PATTERN.test(i))) continue;
|
|
1398
|
+
const id = `csharp-weak-password-encoding:${file}:${call.line}`;
|
|
1399
|
+
if (seen.has(id)) continue;
|
|
1400
|
+
seen.add(id);
|
|
1401
|
+
out.push(makeFinding({
|
|
1402
|
+
ruleId: 'csharp-weak-password-encoding', file, line: call.line, raw, ir,
|
|
1403
|
+
family: 'weak-crypto', severity: 'high', cwe: 'CWE-261',
|
|
1404
|
+
vuln: 'Weak Cryptography for Passwords — Convert.FromBase64String used in place of real decryption',
|
|
1405
|
+
remediation: 'Base64 is an ENCODING, not encryption — it carries no key and is trivially reversible by anyone who can read the encoded bytes. Encrypt stored passwords with a real symmetric cipher (AES-GCM via AesGcm), or better, store only a salted password HASH (PBKDF2/Argon2/bcrypt) and never recover the plaintext at all.',
|
|
1406
|
+
}));
|
|
906
1407
|
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// ─── Entry point ───────────────────────────────────────────────────────────
|
|
1411
|
+
|
|
1412
|
+
// SARD_80_F1 W5.31 — split out of `scanCSharp` so `sast/csharp-cross-file.js`
|
|
1413
|
+
// can run the exact same detector suite a second time against a re-analyzed
|
|
1414
|
+
// `analysis` (one seeded with an externally-tainted parameter from a call in
|
|
1415
|
+
// a DIFFERENT file) without duplicating this call list. `scanCSharp` itself
|
|
1416
|
+
// is unchanged in behavior — it now just builds the IR/analysis and delegates
|
|
1417
|
+
// here, same as before this split.
|
|
1418
|
+
export function runCSharpDetectors(fp, raw, ir, analysis) {
|
|
907
1419
|
const out = [];
|
|
908
1420
|
const seen = new Set();
|
|
909
1421
|
try { detectSqlInjection(fp, raw, ir, analysis, out, seen); } catch {}
|
|
@@ -912,6 +1424,9 @@ export function scanCSharp(fp, raw) {
|
|
|
912
1424
|
try { detectWeakCrypto(fp, raw, ir, analysis, out, seen); } catch {}
|
|
913
1425
|
try { detectWeakRng(fp, raw, ir, analysis, out, seen); } catch {}
|
|
914
1426
|
try { detectHardcodedSecret(fp, raw, ir, analysis, out, seen); } catch {}
|
|
1427
|
+
try { detectCleartextStorage(fp, raw, ir, analysis, out, seen); } catch {}
|
|
1428
|
+
try { detectCleartextNetworkCredential(fp, raw, ir, analysis, out, seen); } catch {}
|
|
1429
|
+
try { detectCleartextNetworkSend(fp, raw, ir, analysis, out, seen); } catch {}
|
|
915
1430
|
try { detectInsecureCookies(fp, raw, ir, analysis, out, seen); } catch {}
|
|
916
1431
|
try { detectXss(fp, raw, ir, analysis, out, seen); } catch {}
|
|
917
1432
|
try { detectXssExpanded(fp, raw, ir, analysis, out, seen); } catch {}
|
|
@@ -925,6 +1440,9 @@ export function scanCSharp(fp, raw) {
|
|
|
925
1440
|
try { detectXxe(fp, raw, ir, analysis, out, seen); } catch {}
|
|
926
1441
|
try { detectXpathInjection(fp, raw, ir, analysis, out, seen); } catch {}
|
|
927
1442
|
try { detectInsecureHttp(fp, raw, ir, analysis, out, seen); } catch {}
|
|
1443
|
+
try { detectUnprotectedCredTransport(fp, raw, ir, analysis, out, seen); } catch {}
|
|
1444
|
+
try { detectPersistentCookie(fp, raw, ir, analysis, out, seen); } catch {}
|
|
1445
|
+
try { detectWeakPasswordEncoding(fp, raw, ir, analysis, out, seen); } catch {}
|
|
928
1446
|
// Stamp route + auth context on every finding for downstream exploitability.
|
|
929
1447
|
for (const f of out) {
|
|
930
1448
|
f._routes = analysis.routes.map(r => ({ http: r.http, path: r.path, line: r.line, requiresAuth: r.requiresAuth, methodName: r.methodName }));
|
|
@@ -938,4 +1456,18 @@ export function scanCSharp(fp, raw) {
|
|
|
938
1456
|
return out;
|
|
939
1457
|
}
|
|
940
1458
|
|
|
1459
|
+
export function scanCSharp(fp, raw) {
|
|
1460
|
+
if (!/\.cs$/i.test(fp)) return [];
|
|
1461
|
+
if (!raw || raw.length > 500_000) return [];
|
|
1462
|
+
let ir, analysis;
|
|
1463
|
+
try {
|
|
1464
|
+
ir = buildCSharpIR(raw);
|
|
1465
|
+
analysis = analyzeCSharpIR(ir);
|
|
1466
|
+
} catch (e) {
|
|
1467
|
+
// IR build failed — fail-closed; better to miss than to throw.
|
|
1468
|
+
return [];
|
|
1469
|
+
}
|
|
1470
|
+
return runCSharpDetectors(fp, raw, ir, analysis);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
941
1473
|
export { buildCSharpIR, analyzeCSharpIR };
|