@clear-capabilities/agentic-security-scanner 0.79.0 → 0.80.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/dist/178.index.js +1 -1
- package/dist/333.index.js +283 -0
- package/dist/384.index.js +1 -1
- package/dist/637.index.js +1 -1
- package/dist/838.index.js +1 -1
- package/dist/985.index.js +90 -1
- package/dist/agentic-security.mjs +83 -83
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +6 -4
- package/src/.agentic-security/findings.json +104638 -0
- package/src/.agentic-security/last-scan.json +104638 -0
- package/src/.agentic-security/last-scan.json.sig +1 -0
- package/src/.agentic-security/scan-history.json +12562 -0
- package/src/.agentic-security/streak.json +21 -0
- package/src/dataflow/.agentic-security/findings.json +6086 -0
- package/src/dataflow/.agentic-security/last-scan.json +6086 -0
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
- package/src/dataflow/.agentic-security/scan-history.json +250 -0
- package/src/dataflow/.agentic-security/streak.json +21 -0
- package/src/dataflow/cross-service-taint.js +201 -0
- package/src/dataflow/formal-verify.js +204 -0
- package/src/dataflow/ifds-precise.js +222 -0
- package/src/dataflow/k2-summary-cache.js +153 -0
- package/src/dataflow/lib-taint-summaries.js +198 -0
- package/src/dataflow/privacy-taint.js +205 -0
- package/src/dataflow/smt-feasibility.js +189 -0
- package/src/engine.js +784 -127
- package/src/ir/.agentic-security/findings.json +4011 -0
- package/src/ir/.agentic-security/last-scan.json +4011 -0
- package/src/ir/.agentic-security/last-scan.json.sig +1 -0
- package/src/ir/.agentic-security/scan-history.json +193 -0
- package/src/ir/.agentic-security/streak.json +20 -0
- package/src/ir/cpp-preprocessor.js +142 -0
- package/src/ir/csharp-ir.js +604 -0
- package/src/ir/universal-ir.js +403 -0
- package/src/mcp/.agentic-security/findings.json +8632 -0
- package/src/mcp/.agentic-security/last-scan.json +8632 -0
- package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
- package/src/mcp/.agentic-security/scan-history.json +143 -0
- package/src/mcp/.agentic-security/streak.json +20 -0
- package/src/mcp/tools.js +90 -1
- package/src/posture/.agentic-security/findings.json +64004 -0
- package/src/posture/.agentic-security/last-scan.json +64004 -0
- package/src/posture/.agentic-security/last-scan.json.sig +1 -0
- package/src/posture/.agentic-security/scan-history.json +7162 -0
- package/src/posture/.agentic-security/streak.json +21 -0
- package/src/posture/api-contract.js +193 -0
- package/src/posture/attack-taxonomy.js +227 -0
- package/src/posture/compliance-policy.js +218 -0
- package/src/posture/composite-risk.js +122 -0
- package/src/posture/csharp-analysis.js +330 -0
- package/src/posture/exploit-bundle.js +210 -0
- package/src/posture/federated-learning.js +172 -0
- package/src/posture/license-attributions.js +94 -0
- package/src/posture/license-graph.js +238 -0
- package/src/posture/pqc-migration-plan.js +158 -0
- package/src/posture/reachability-filter.js +33 -2
- package/src/posture/realtime-cve-monitor.js +214 -0
- package/src/posture/runtime-correlation.js +174 -0
- package/src/posture/sbom-diff.js +171 -0
- package/src/posture/sca-policy.js +235 -0
- package/src/posture/sca-upgrade.js +259 -0
- package/src/posture/threat-model-auto.js +268 -0
- package/src/posture/triage-learning.js +170 -0
- package/src/posture/triage.js +26 -1
- package/src/sast/.agentic-security/findings.json +6154 -0
- package/src/sast/.agentic-security/last-scan.json +6154 -0
- package/src/sast/.agentic-security/last-scan.json.sig +1 -0
- package/src/sast/.agentic-security/scan-history.json +941 -0
- package/src/sast/.agentic-security/streak.json +22 -0
- package/src/sast/_secret-entropy.js +145 -0
- package/src/sast/cloud-iam.js +312 -0
- package/src/sast/cpp.js +138 -4
- package/src/sast/crypto-protocol.js +388 -0
- package/src/sast/csharp-tokenizer.js +392 -0
- package/src/sast/csharp.js +924 -138
- package/src/sast/dapp-frontend.js +200 -0
- package/src/sast/k8s-admission.js +271 -0
- package/src/sast/llm-app.js +272 -0
- package/src/sast/ml-supply-chain.js +259 -0
- package/src/sast/mobile.js +224 -0
- package/src/sast/post-quantum-crypto.js +348 -0
- package/src/sast/web3-advanced.js +375 -0
- package/src/sca/.agentic-security/findings.json +7460 -0
- package/src/sca/.agentic-security/last-scan.json +7460 -0
- package/src/sca/.agentic-security/last-scan.json.sig +1 -0
- package/src/sca/.agentic-security/scan-history.json +113 -0
- package/src/sca/.agentic-security/streak.json +21 -0
- package/src/sca/CLAUDE.md +161 -0
- package/src/sca/binary-metadata.js +37 -15
- package/src/sca/sigstore-verify.js +215 -0
package/src/sast/csharp.js
CHANGED
|
@@ -1,152 +1,938 @@
|
|
|
1
|
-
// C# / .NET SAST module.
|
|
1
|
+
// C# / .NET SAST module — Layers 1-4 of the C# detection pipeline.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// Each rule fires ONLY on the unsafe shape and has a safe-shape detector
|
|
5
|
-
// where applicable — keeps precision high and avoids polluting clean repos
|
|
6
|
-
// with low-signal warnings.
|
|
3
|
+
// Architecture (replacing the previous regex-on-cleaned-source approach):
|
|
7
4
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// -
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
5
|
+
// Layer 1: Token-aware lexer (../sast/csharp-tokenizer.js) — strings,
|
|
6
|
+
// verbatim, interpolated, comments, attributes all preserved
|
|
7
|
+
// with semantic identity.
|
|
8
|
+
// Layer 2: Hand-rolled IR (../ir/csharp-ir.js) — emits classes, methods,
|
|
9
|
+
// calls, declarations, assignments, attributes.
|
|
10
|
+
// Layer 3: Lexical type-flow + taint (../posture/csharp-analysis.js) —
|
|
11
|
+
// per-method typeMap and taintMap, forward-propagated through
|
|
12
|
+
// declarations and assignments.
|
|
13
|
+
// Layer 4: Attribute-driven route + auth detection — same module, reads
|
|
14
|
+
// [HttpGet]/[HttpPost]/[Route]/[Authorize]/[AllowAnonymous].
|
|
15
|
+
//
|
|
16
|
+
// The detectors below query the IR using these helpers and are completely
|
|
17
|
+
// regex-free for their primary logic. They still use small regex for cheap
|
|
18
|
+
// type-name and string-content checks.
|
|
19
|
+
//
|
|
20
|
+
// Covered Juliet C# CWE families:
|
|
21
|
+
// CWE-22 path-traversal Path.Combine + tainted segment, no Path.GetFullPath check
|
|
22
|
+
// CWE-78 command-injection Process.Start with tainted args / ShellExecute=true
|
|
23
|
+
// CWE-79 xss Razor Html.Raw / Response.Write with tainted argument
|
|
24
|
+
// CWE-89 sql-injection Sql/Ole/MySql/NpgsqlCommand with tainted CommandText / concatenation in constructor / FromSqlRaw
|
|
25
|
+
// CWE-90 ldap-injection DirectorySearcher / LdapConnection .Search/.SendRequest with tainted filter
|
|
26
|
+
// CWE-330 weak-rng new Random() in cryptographic context (presence of System.Security.Cryptography uses or "password"/"token" naming)
|
|
27
|
+
// CWE-327 weak-crypto DESCryptoServiceProvider / RC2 / TripleDES / MD5 / SHA1 — including factory methods
|
|
28
|
+
// CWE-502 insecure-deserialization BinaryFormatter.Deserialize / NetDataContractSerializer / Newtonsoft TypeNameHandling != None
|
|
29
|
+
// CWE-611 xxe XmlDocument w/o XmlResolver=null; XmlReaderSettings w/o DtdProcessing=Prohibit
|
|
30
|
+
// CWE-798 hardcoded-secret Field/local with name matching password/token/secret/apiKey + non-empty string literal initializer
|
|
31
|
+
// CWE-1004 header-hardening new HttpCookie missing Secure/HttpOnly
|
|
32
|
+
// CWE-22 validate-input-false [ValidateInput(false)] attribute
|
|
33
|
+
//
|
|
34
|
+
// Findings carry: id, file, line, vuln, severity, cwe, stride, snippet,
|
|
35
|
+
// remediation, confidence, parser, family, and a `_taintEvidence` field
|
|
36
|
+
// when the detector relied on Layer 3 to confirm reachability from a
|
|
37
|
+
// known source. The LLM validator (when enabled) sees these fields and
|
|
38
|
+
// can second-stage low-confidence findings.
|
|
39
|
+
|
|
40
|
+
import { buildCSharpIR } from '../ir/csharp-ir.js';
|
|
41
|
+
import {
|
|
42
|
+
analyzeCSharpIR, receiverIsType, expressionIsTainted, interpStringIsTainted, argIsTainted,
|
|
43
|
+
} from '../posture/csharp-analysis.js';
|
|
44
|
+
|
|
45
|
+
// Helper: collect identifier names from a token slice (idents only — not
|
|
46
|
+
// string-literal contents). Used by detectors when checking taint on a
|
|
47
|
+
// declaration's rhs to avoid false positives from SQL parameter
|
|
48
|
+
// placeholders like "@id" appearing inside a string literal.
|
|
49
|
+
function rhsIdents(tokens) {
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const t of tokens || []) {
|
|
52
|
+
if (!t) continue;
|
|
53
|
+
if (t.kind === 'ident') out.push(t.value);
|
|
54
|
+
if (t.kind === 'interp') for (const p of t.parts || []) if (p.kind === 'expr') for (const inner of (p.tokens || [])) if (inner.kind === 'ident') out.push(inner.value);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function rhsHasConcatWithIdent(tokens) {
|
|
60
|
+
// True if the token stream contains: <string-literal> '+' <ident>
|
|
61
|
+
// (idents from inside the string don't count).
|
|
62
|
+
for (let i = 0; i < tokens.length - 2; i++) {
|
|
63
|
+
if ((tokens[i].kind === 'string' || tokens[i].kind === 'verbatim') &&
|
|
64
|
+
tokens[i + 1].kind === 'op' && tokens[i + 1].value === '+' &&
|
|
65
|
+
tokens[i + 2].kind === 'ident') return true;
|
|
66
|
+
if (tokens[i].kind === 'ident' && tokens[i + 1].kind === 'op' && tokens[i + 1].value === '+' &&
|
|
67
|
+
(tokens[i + 2].kind === 'string' || tokens[i + 2].kind === 'verbatim')) return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function rhsHasInterp(tokens) {
|
|
73
|
+
return (tokens || []).some(t => t.kind === 'interp');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const SQL_COMMAND_TYPES = /^(?:System\.Data\.SqlClient\.)?(?:Sql|OleDb|MySql|Npgsql|SQLite)Command$/;
|
|
77
|
+
const SQL_EXEC_METHODS = /^(?:Execute(?:Reader|Scalar|NonQuery|DbDataReader|Reader)?(?:Async)?)$/;
|
|
78
|
+
const LDAP_SEARCH_TYPES = /^(?:DirectorySearcher|LdapConnection)$/;
|
|
79
|
+
const LDAP_SEARCH_METHODS = /^(?:Search|FindOne|FindAll|SendRequest)$/;
|
|
80
|
+
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(?:DES|TripleDES|RC2|MD5|SHA1)\.Create\b/;
|
|
82
|
+
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
|
+
const PATH_TRAVERSAL_BASES_SANITIZER = /\bPath\.GetFullPath\b/;
|
|
84
|
+
const XSS_SAFE_SINK_PATTERN = /\bHtmlEncode\b|\bHtmlEncoder\b|\bAntiXss/;
|
|
85
|
+
|
|
86
|
+
function makeFinding({ ruleId, file, line, raw, ir, family, severity, cwe, vuln, remediation, evidence, confidence = 0.85 }) {
|
|
87
|
+
const stride = (cwe === 'CWE-89' ? 'Tampering'
|
|
88
|
+
: cwe === 'CWE-78' ? 'Elevation of Privilege'
|
|
89
|
+
: cwe === 'CWE-79' ? 'Tampering'
|
|
90
|
+
: cwe === 'CWE-611' ? 'Information Disclosure'
|
|
91
|
+
: cwe === 'CWE-502' ? 'Elevation of Privilege'
|
|
92
|
+
: cwe === 'CWE-22' ? 'Tampering'
|
|
93
|
+
: cwe === 'CWE-327' ? 'Information Disclosure'
|
|
94
|
+
: cwe === 'CWE-330' ? 'Spoofing'
|
|
95
|
+
: cwe === 'CWE-798' ? 'Information Disclosure'
|
|
96
|
+
: cwe === 'CWE-1004'? 'Information Disclosure'
|
|
97
|
+
: cwe === 'CWE-90' ? 'Tampering'
|
|
98
|
+
: 'Tampering');
|
|
99
|
+
return {
|
|
100
|
+
id: `${ruleId}:${file}:${line}`, file, line,
|
|
101
|
+
vuln, severity, cwe, stride, family,
|
|
102
|
+
snippet: ((raw && raw.split('\n')[line - 1]) || '').trim().slice(0, 200),
|
|
103
|
+
remediation,
|
|
104
|
+
confidence,
|
|
105
|
+
parser: 'CSHARP',
|
|
106
|
+
_taintEvidence: evidence || null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Detectors ──────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
function detectSqlInjection(file, raw, ir, analysis, out, seen) {
|
|
113
|
+
for (const m of ir.methods) {
|
|
114
|
+
const flow = analysis.methodFlow.get(m);
|
|
115
|
+
if (!flow) continue;
|
|
116
|
+
// 1. SqlCommand-style ctor with tainted concatenation in the first arg.
|
|
117
|
+
for (const decl of m.decls) {
|
|
118
|
+
if (!SQL_COMMAND_TYPES.test(decl.type || '') && !(decl.isVar && SQL_COMMAND_TYPES.test((decl.rhsText.match(/^\s*new\s+(\w+)/) || [])[1] || ''))) continue;
|
|
119
|
+
if (!decl.rhsTokens) continue;
|
|
120
|
+
// Use token-aware checks so SQL parameter placeholders like "@id"
|
|
121
|
+
// inside string literals are NOT counted as code identifiers.
|
|
122
|
+
const hasConcatWithIdent = rhsHasConcatWithIdent(decl.rhsTokens);
|
|
123
|
+
const hasInterp = rhsHasInterp(decl.rhsTokens);
|
|
124
|
+
if (!hasConcatWithIdent && !hasInterp) continue;
|
|
125
|
+
const idents = rhsIdents(decl.rhsTokens);
|
|
126
|
+
const ref = idents.find(r => flow.taintMap.get(r));
|
|
127
|
+
if (!ref) continue;
|
|
128
|
+
const id = `csharp-sql-ctor:${file}:${decl.line}`;
|
|
129
|
+
if (seen.has(id)) continue;
|
|
130
|
+
seen.add(id);
|
|
131
|
+
out.push(makeFinding({
|
|
132
|
+
ruleId: 'csharp-sql-ctor', file, line: decl.line, raw, ir,
|
|
133
|
+
family: 'sql-injection', severity: 'high', cwe: 'CWE-89',
|
|
134
|
+
vuln: 'SQL Injection — SqlCommand built from tainted concatenation/interpolation',
|
|
135
|
+
remediation: 'Use parameterized queries: `var cmd = new SqlCommand("SELECT * FROM users WHERE id = @id", conn); cmd.Parameters.AddWithValue("@id", id);`. Interpolated strings ($"…") are pre-rendered before reaching the parameterizer.',
|
|
136
|
+
evidence: { type: 'taint', taintedRef: ref, decl: decl.fullTarget || decl.name },
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
// 2. cmd.ExecuteX call where cmd's CommandText was assigned a tainted value.
|
|
140
|
+
for (const call of m.calls) {
|
|
141
|
+
if (!SQL_EXEC_METHODS.test(call.method || '')) continue;
|
|
142
|
+
const receiver = call.receiver;
|
|
143
|
+
if (!receiver) continue;
|
|
144
|
+
if (!receiverIsType(m, flow, receiver, SQL_COMMAND_TYPES)) continue;
|
|
145
|
+
// Find the most recent CommandText assignment for this receiver.
|
|
146
|
+
const cmdTextAssign = (m.assignments || [])
|
|
147
|
+
.filter(a => a.target === receiver && a.memberPath === 'CommandText')
|
|
148
|
+
.slice(-1)[0];
|
|
149
|
+
// Also check the ctor first-arg if no CommandText set.
|
|
150
|
+
const decl = (m.decls || []).find(d => d.name === receiver);
|
|
151
|
+
// Use token-aware idents extraction so SQL parameter placeholders
|
|
152
|
+
// inside string literals (e.g. "@id") aren't flagged as code refs.
|
|
153
|
+
const cmdAssignIdents = cmdTextAssign ? rhsIdents(cmdTextAssign.rhsTokens) : [];
|
|
154
|
+
const declRhsIdents = decl ? rhsIdents(decl.rhsTokens) : [];
|
|
155
|
+
// Concat check via tokens too.
|
|
156
|
+
const cmdAssignHasConcat = cmdTextAssign && (rhsHasConcatWithIdent(cmdTextAssign.rhsTokens) || rhsHasInterp(cmdTextAssign.rhsTokens));
|
|
157
|
+
const declHasConcat = decl && (rhsHasConcatWithIdent(decl.rhsTokens) || rhsHasInterp(decl.rhsTokens));
|
|
158
|
+
const cmdAssignTainted = cmdAssignHasConcat && cmdAssignIdents.some(r => flow.taintMap.get(r));
|
|
159
|
+
const declTainted = declHasConcat && declRhsIdents.some(r => flow.taintMap.get(r));
|
|
160
|
+
if (!cmdAssignTainted && !declTainted) continue;
|
|
161
|
+
const id = `csharp-sql-exec:${file}:${call.line}`;
|
|
162
|
+
if (seen.has(id)) continue;
|
|
163
|
+
seen.add(id);
|
|
164
|
+
out.push(makeFinding({
|
|
165
|
+
ruleId: 'csharp-sql-exec', file, line: call.line, raw, ir,
|
|
166
|
+
family: 'sql-injection', severity: 'high', cwe: 'CWE-89',
|
|
167
|
+
vuln: 'SQL Injection — SqlCommand.ExecuteX with tainted CommandText',
|
|
168
|
+
remediation: 'Bind every user-supplied value via `cmd.Parameters.AddWithValue("@p", value)` and use `@p` placeholders in the SQL. Never compose CommandText with `+`, `string.Format`, or `$"…"`.',
|
|
169
|
+
evidence: { type: 'taint', receiver, callLine: call.line, cmdTextLine: cmdTextAssign?.line || decl?.line },
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
// 3. FromSqlRaw / FromSql with concat or interpolation.
|
|
173
|
+
for (const call of m.calls) {
|
|
174
|
+
if (!/^FromSql(?:Raw)?$/.test(call.method)) continue;
|
|
175
|
+
if (!call.args.length) continue;
|
|
176
|
+
const arg0 = call.args[0];
|
|
177
|
+
if (interpStringIsTainted(flow, arg0.tokens.find(t => t.kind === 'interp'))) {
|
|
178
|
+
const id = `csharp-ef-fromsql-interp:${file}:${call.line}`;
|
|
179
|
+
if (seen.has(id)) continue;
|
|
180
|
+
seen.add(id);
|
|
181
|
+
out.push(makeFinding({
|
|
182
|
+
ruleId: 'csharp-ef-fromsql-interp', file, line: call.line, raw, ir,
|
|
183
|
+
family: 'sql-injection', severity: 'high', cwe: 'CWE-89',
|
|
184
|
+
vuln: 'SQL Injection — EF Core FromSqlRaw with tainted interpolation',
|
|
185
|
+
remediation: 'Use `FromSqlInterpolated($"...")` or `FromSqlRaw("...", parameter)` so EF parameterizes values. `FromSqlRaw($"…{var}…")` evaluates the interpolation BEFORE EF sees it.',
|
|
186
|
+
}));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (/["'][^"']*["']\s*\+/.test(arg0.text) && arg0.idents.some(r => flow.taintMap.get(r))) {
|
|
190
|
+
const id = `csharp-ef-fromsql-concat:${file}:${call.line}`;
|
|
191
|
+
if (seen.has(id)) continue;
|
|
192
|
+
seen.add(id);
|
|
193
|
+
out.push(makeFinding({
|
|
194
|
+
ruleId: 'csharp-ef-fromsql-concat', file, line: call.line, raw, ir,
|
|
195
|
+
family: 'sql-injection', severity: 'high', cwe: 'CWE-89',
|
|
196
|
+
vuln: 'SQL Injection — EF Core FromSqlRaw with concatenated tainted value',
|
|
197
|
+
remediation: 'Use `FromSqlInterpolated($"… {value}")` or `FromSqlRaw("… {0}", value)` so EF parameterizes. String concatenation defeats the protection.',
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function detectCommandInjection(file, raw, ir, analysis, out, seen) {
|
|
205
|
+
for (const m of ir.methods) {
|
|
206
|
+
const flow = analysis.methodFlow.get(m);
|
|
207
|
+
if (!flow) continue;
|
|
208
|
+
// Process.Start(...) — 2-arg form OR ProcessStartInfo with tainted Arguments
|
|
209
|
+
for (const call of m.calls) {
|
|
210
|
+
if (!(call.fullPath === 'Process.Start' || /\bProcessStartInfo\b/.test(call.fullPath))) {
|
|
211
|
+
// also catch piping a ProcessStartInfo
|
|
212
|
+
}
|
|
213
|
+
if (call.fullPath === 'Process.Start' && call.args.length >= 2) {
|
|
214
|
+
const arg1 = call.args[1];
|
|
215
|
+
if (expressionIsTainted(flow, arg1.text)) {
|
|
216
|
+
const id = `csharp-proc-start2:${file}:${call.line}`;
|
|
217
|
+
if (seen.has(id)) continue;
|
|
218
|
+
seen.add(id);
|
|
219
|
+
out.push(makeFinding({
|
|
220
|
+
ruleId: 'csharp-proc-start2', file, line: call.line, raw, ir,
|
|
221
|
+
family: 'command-injection', severity: 'critical', cwe: 'CWE-78',
|
|
222
|
+
vuln: 'Command Injection — Process.Start with tainted argument string',
|
|
223
|
+
remediation: 'Use `ProcessStartInfo` with `ArgumentList` (an `IList<string>`) so each argument is escaped individually. Never compose a single argument string from user input.',
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// PSI initializer pattern: var psi = new ProcessStartInfo { UseShellExecute=true, Arguments = tainted }
|
|
229
|
+
for (const decl of m.decls) {
|
|
230
|
+
if (decl.type === 'ProcessStartInfo' || /^new\s+ProcessStartInfo\b/.test(decl.rhsText || '')) {
|
|
231
|
+
const rhs = decl.rhsText || '';
|
|
232
|
+
const hasShell = /\bUseShellExecute\s*=\s*true\b/.test(rhs);
|
|
233
|
+
const argsMatch = rhs.match(/\bArguments\s*=\s*([^,}]+)/);
|
|
234
|
+
if (hasShell && argsMatch && expressionIsTainted(flow, argsMatch[1])) {
|
|
235
|
+
const id = `csharp-psi-shell-tainted:${file}:${decl.line}`;
|
|
236
|
+
if (seen.has(id)) continue;
|
|
237
|
+
seen.add(id);
|
|
238
|
+
out.push(makeFinding({
|
|
239
|
+
ruleId: 'csharp-psi-shell-tainted', file, line: decl.line, raw, ir,
|
|
240
|
+
family: 'command-injection', severity: 'critical', cwe: 'CWE-78',
|
|
241
|
+
vuln: 'Command Injection — ProcessStartInfo with UseShellExecute=true and tainted Arguments',
|
|
242
|
+
remediation: 'Set `UseShellExecute = false` and pass arguments as a `string[]` via `ProcessStartInfo.ArgumentList`. ShellExecute=true routes the call through cmd.exe / the shell, where any user-supplied metacharacter is interpreted.',
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function detectInsecureDeserialization(file, raw, ir, analysis, out, seen) {
|
|
251
|
+
for (const m of ir.methods) {
|
|
252
|
+
// BinaryFormatter ctor anywhere is sufficient.
|
|
253
|
+
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
|
+
if (/\bTypeNameHandling\s*=\s*TypeNameHandling\.(?:All|Auto|Objects|Arrays)\b/.test(decl.rhsText || '')) {
|
|
266
|
+
const id = `csharp-newtonsoft-typename:${file}:${decl.line}`;
|
|
267
|
+
if (seen.has(id)) continue;
|
|
268
|
+
seen.add(id);
|
|
269
|
+
out.push(makeFinding({
|
|
270
|
+
ruleId: 'csharp-newtonsoft-typename', file, line: decl.line, raw, ir,
|
|
271
|
+
family: 'insecure-deserialization', severity: 'critical', cwe: 'CWE-502',
|
|
272
|
+
vuln: 'Insecure Deserialization — Newtonsoft.Json TypeNameHandling != None',
|
|
273
|
+
remediation: 'TypeNameHandling.All/Auto/Objects/Arrays enables RCE via gadget chains. Set `TypeNameHandling.None` or migrate to System.Text.Json.',
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Also catch BinaryFormatter as a call: bf.Deserialize(stream)
|
|
278
|
+
for (const call of m.calls) {
|
|
279
|
+
const flow = analysis.methodFlow.get(m);
|
|
280
|
+
if (call.method === 'Deserialize' && flow && receiverIsType(m, flow, call.receiver, 'BinaryFormatter')) {
|
|
281
|
+
const id = `csharp-binformatter-call:${file}:${call.line}`;
|
|
282
|
+
if (seen.has(id)) continue;
|
|
283
|
+
seen.add(id);
|
|
284
|
+
out.push(makeFinding({
|
|
285
|
+
ruleId: 'csharp-binformatter-call', file, line: call.line, raw, ir,
|
|
286
|
+
family: 'insecure-deserialization', severity: 'critical', cwe: 'CWE-502',
|
|
287
|
+
vuln: 'Insecure Deserialization — BinaryFormatter.Deserialize call',
|
|
288
|
+
remediation: 'Drop BinaryFormatter entirely. Use System.Text.Json or DataContractJsonSerializer with KnownTypes.',
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function detectWeakCrypto(file, raw, ir, analysis, out, seen) {
|
|
296
|
+
// 1. new DES/MD5/SHA1/RC2/TripleDESCryptoServiceProvider() / new MD5Managed()
|
|
297
|
+
for (const decl of ir.decls) {
|
|
298
|
+
if (decl.rhsText && WEAK_CRYPTO_FACTORY_PATTERN.test(decl.rhsText)) {
|
|
299
|
+
const id = `csharp-weak-crypto-factory:${file}:${decl.line}`;
|
|
300
|
+
if (seen.has(id)) continue;
|
|
301
|
+
seen.add(id);
|
|
302
|
+
out.push(makeFinding({
|
|
303
|
+
ruleId: 'csharp-weak-crypto-factory', file, line: decl.line, raw, ir,
|
|
304
|
+
family: 'weak-crypto', severity: 'high', cwe: 'CWE-327',
|
|
305
|
+
vuln: 'Weak Cryptography — MD5/SHA1/DES/3DES/RC2 factory method',
|
|
306
|
+
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
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
const ctorMatch = (decl.rhsText || '').match(/\bnew\s+([A-Z]\w+)\s*\(/);
|
|
310
|
+
if (ctorMatch && WEAK_CRYPTO_TYPES.test(ctorMatch[1])) {
|
|
311
|
+
const id = `csharp-weak-crypto-ctor:${file}:${decl.line}`;
|
|
312
|
+
if (seen.has(id)) continue;
|
|
313
|
+
seen.add(id);
|
|
314
|
+
out.push(makeFinding({
|
|
315
|
+
ruleId: 'csharp-weak-crypto-ctor', file, line: decl.line, raw, ir,
|
|
316
|
+
family: 'weak-crypto', severity: 'high', cwe: 'CWE-327',
|
|
317
|
+
vuln: `Weak Cryptography — \`new ${ctorMatch[1]}()\``,
|
|
318
|
+
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
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function detectWeakRng(file, raw, ir, analysis, out, seen) {
|
|
325
|
+
// Heuristic for "cryptographic context": file declares a hardcoded-secret-shaped
|
|
326
|
+
// variable, references password/token/key names, or uses crypto primitives.
|
|
327
|
+
const fileText = raw || '';
|
|
328
|
+
const looksCrypto = /\b(?:password|passwd|pw|pwd|token|secret|api[_-]?key|salt|nonce|iv)\b/i.test(fileText)
|
|
329
|
+
|| /Cryptography|CryptoServiceProvider|CryptoStream/.test(fileText)
|
|
330
|
+
|| /\bAes(?:Cng|CryptoServiceProvider)?\b|\bRSA(?:CryptoServiceProvider|Cng)?\b/.test(fileText)
|
|
331
|
+
|| /\b(?:DES|TripleDES|RC2|MD5|SHA1|HMACSHA)/.test(fileText);
|
|
332
|
+
if (!looksCrypto) return;
|
|
333
|
+
for (const decl of ir.decls) {
|
|
334
|
+
if (/\bnew\s+Random\s*\(/.test(decl.rhsText || '')) {
|
|
335
|
+
const id = `csharp-weak-rng:${file}:${decl.line}`;
|
|
336
|
+
if (seen.has(id)) continue;
|
|
337
|
+
seen.add(id);
|
|
338
|
+
out.push(makeFinding({
|
|
339
|
+
ruleId: 'csharp-weak-rng', file, line: decl.line, raw, ir,
|
|
340
|
+
family: 'weak-rng', severity: 'high', cwe: 'CWE-330',
|
|
341
|
+
vuln: 'Weak Randomness — System.Random in cryptographic context',
|
|
342
|
+
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.',
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// call site: var x = new Random().Next(); — also caught above via decl.
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function detectHardcodedSecret(file, raw, ir, analysis, out, seen) {
|
|
350
|
+
for (const decl of ir.decls) {
|
|
351
|
+
if (!SECRET_NAME_PATTERN.test(decl.name || '')) continue;
|
|
352
|
+
if (!decl.rhsText) continue;
|
|
353
|
+
// Match a bare string literal as the rhs — and require non-trivial length / shape.
|
|
354
|
+
const m = decl.rhsText.match(/^\s*["']([^"']{6,})["']\s*$/) || decl.rhsText.match(/^\s*@"([^"]{6,})"\s*$/);
|
|
355
|
+
if (!m) continue;
|
|
356
|
+
const val = m[1];
|
|
357
|
+
// Filter common placeholders.
|
|
358
|
+
if (/^(?:changeme|placeholder|todo|tbd|xxxxx|secret|password|your_?password)$/i.test(val)) continue;
|
|
359
|
+
if (/^[A-Za-z]+$/.test(val) && val.length < 12) continue; // single word, too short to be a secret
|
|
360
|
+
const id = `csharp-hardcoded-secret:${file}:${decl.line}`;
|
|
361
|
+
if (seen.has(id)) continue;
|
|
362
|
+
seen.add(id);
|
|
363
|
+
out.push(makeFinding({
|
|
364
|
+
ruleId: 'csharp-hardcoded-secret', file, line: decl.line, raw, ir,
|
|
365
|
+
family: 'hardcoded-secret', severity: 'high', cwe: 'CWE-798',
|
|
366
|
+
vuln: `Hardcoded Secret — \`${decl.name}\` assigned a literal value`,
|
|
367
|
+
remediation: 'Load secrets from environment variables (`Environment.GetEnvironmentVariable`), Azure Key Vault, AWS Secrets Manager, or .NET `Configuration` with user-secrets in development. Never commit literal credentials.',
|
|
368
|
+
confidence: 0.7,
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function detectInsecureCookies(file, raw, ir, analysis, out, seen) {
|
|
374
|
+
// `new HttpCookie(...)` without subsequent `.Secure = true` or `.HttpOnly = true` in the same method scope.
|
|
375
|
+
for (const m of ir.methods) {
|
|
376
|
+
const cookieDecls = m.decls.filter(d => /\bnew\s+HttpCookie\s*\(/.test(d.rhsText || ''));
|
|
377
|
+
if (!cookieDecls.length) continue;
|
|
378
|
+
for (const cd of cookieDecls) {
|
|
379
|
+
const setSecure = m.assignments.some(a => a.target === cd.name && a.memberPath === 'Secure' && /\btrue\b/.test(a.rhsText));
|
|
380
|
+
const setHttpOnly = m.assignments.some(a => a.target === cd.name && a.memberPath === 'HttpOnly' && /\btrue\b/.test(a.rhsText));
|
|
381
|
+
if (setSecure && setHttpOnly) continue;
|
|
382
|
+
const missing = !setSecure && !setHttpOnly ? '.Secure and .HttpOnly'
|
|
383
|
+
: !setSecure ? '.Secure'
|
|
384
|
+
: '.HttpOnly';
|
|
385
|
+
const id = `csharp-cookie-flags:${file}:${cd.line}`;
|
|
386
|
+
if (seen.has(id)) continue;
|
|
387
|
+
seen.add(id);
|
|
388
|
+
out.push(makeFinding({
|
|
389
|
+
ruleId: 'csharp-cookie-flags', file, line: cd.line, raw, ir,
|
|
390
|
+
family: 'header-hardening', severity: 'medium', cwe: 'CWE-1004',
|
|
391
|
+
vuln: `Insecure Cookie — HttpCookie missing ${missing} flag`,
|
|
392
|
+
remediation: 'Set both `cookie.Secure = true;` and `cookie.HttpOnly = true;` before adding to the response. Use `.SameSite = SameSiteMode.Lax` (or Strict) to defeat CSRF.',
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// XSS sinks — narrower receiver match using argIsTainted so SQL-style
|
|
399
|
+
// "@id" placeholder identifiers don't FP-fire from string contents.
|
|
400
|
+
const XSS_SINKS = [
|
|
401
|
+
{ method: 'Raw', receivers: ['Html', '@Html'], note: 'Razor Html.Raw' },
|
|
402
|
+
{ method: 'Write', receivers: ['Response', 'HttpContext.Response', 'context.Response', 'this.Response'], note: 'Response.Write' },
|
|
403
|
+
{ method: 'WriteLine', receivers: ['Response', 'HttpContext.Response', 'Response.Output'], note: 'Response.Output.WriteLine' },
|
|
404
|
+
{ method: 'Output', receivers: null /* property — handled separately */, note: 'Response.Output property access' },
|
|
113
405
|
];
|
|
114
406
|
|
|
115
|
-
function
|
|
407
|
+
function detectXss(file, raw, ir, analysis, out, seen) {
|
|
408
|
+
for (const m of ir.methods) {
|
|
409
|
+
const flow = analysis.methodFlow.get(m);
|
|
410
|
+
if (!flow) continue;
|
|
411
|
+
for (const call of m.calls) {
|
|
412
|
+
// 1. Html.Raw / @Html.Raw
|
|
413
|
+
if (call.method === 'Raw' && (call.receiver === 'Html' || call.receiver === '@Html')) {
|
|
414
|
+
const arg = call.args[0];
|
|
415
|
+
if (!arg) continue;
|
|
416
|
+
if (XSS_SAFE_SINK_PATTERN.test(arg.text)) continue;
|
|
417
|
+
if (!argIsTainted(flow, arg)) continue;
|
|
418
|
+
const id = `csharp-htmlraw:${file}:${call.line}`;
|
|
419
|
+
if (seen.has(id)) continue;
|
|
420
|
+
seen.add(id);
|
|
421
|
+
out.push(makeFinding({
|
|
422
|
+
ruleId: 'csharp-htmlraw', file, line: call.line, raw, ir,
|
|
423
|
+
family: 'xss', severity: 'high', cwe: 'CWE-79',
|
|
424
|
+
vuln: 'XSS — Razor Html.Raw with tainted value',
|
|
425
|
+
remediation: '`@Html.Raw(x)` emits `x` without HTML-encoding. Use `@x` (Razor auto-encodes), or pass through `HtmlEncoder.Default.Encode(x)` / `HttpUtility.HtmlEncode(x)` / `HtmlSanitizer.Sanitize(x)`.',
|
|
426
|
+
}));
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
// 2. Response.Write / Response.Output.Write / Response.WriteAsync
|
|
430
|
+
if (/^Write(?:Async|Line)?$/.test(call.method)
|
|
431
|
+
&& (call.receiver === 'Response' || call.receiver === 'HttpContext.Response'
|
|
432
|
+
|| call.receiver === 'context.Response' || call.receiver === 'this.Response'
|
|
433
|
+
|| call.receiver === 'Response.Output' || call.receiver === 'Response.OutputStream')) {
|
|
434
|
+
const arg = call.args[0];
|
|
435
|
+
if (!arg) continue;
|
|
436
|
+
if (XSS_SAFE_SINK_PATTERN.test(arg.text)) continue;
|
|
437
|
+
if (!argIsTainted(flow, arg)) continue;
|
|
438
|
+
const id = `csharp-response-write:${file}:${call.line}`;
|
|
439
|
+
if (seen.has(id)) continue;
|
|
440
|
+
seen.add(id);
|
|
441
|
+
out.push(makeFinding({
|
|
442
|
+
ruleId: 'csharp-response-write', file, line: call.line, raw, ir,
|
|
443
|
+
family: 'xss', severity: 'high', cwe: 'CWE-79',
|
|
444
|
+
vuln: `XSS — ${call.fullPath || (call.receiver + '.' + call.method)} with tainted value`,
|
|
445
|
+
remediation: 'Encode the value via `HttpUtility.HtmlEncode(x)` (or `HtmlEncoder.Default.Encode(x)` in ASP.NET Core) before writing. Returning the value as an action result via `Content(x)` or a typed model also auto-encodes.',
|
|
446
|
+
}));
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
// 3. Writer.Write(tainted) where writer was assigned from Response.Output
|
|
450
|
+
// or any HttpResponse-derived getter. Best-effort: receiver name pattern.
|
|
451
|
+
if (/^Write(?:Async|Line)?$/.test(call.method) && call.receiver && /Writer|writer|output/i.test(call.receiver)) {
|
|
452
|
+
const arg = call.args[0];
|
|
453
|
+
if (!arg) continue;
|
|
454
|
+
if (XSS_SAFE_SINK_PATTERN.test(arg.text)) continue;
|
|
455
|
+
if (!argIsTainted(flow, arg)) continue;
|
|
456
|
+
const t = flow.typeMap.get(call.receiver);
|
|
457
|
+
if (t && !/Writer|TextWriter|HtmlTextWriter|StringBuilder/i.test(t)) continue;
|
|
458
|
+
const id = `csharp-writer-tainted:${file}:${call.line}`;
|
|
459
|
+
if (seen.has(id)) continue;
|
|
460
|
+
seen.add(id);
|
|
461
|
+
out.push(makeFinding({
|
|
462
|
+
ruleId: 'csharp-writer-tainted', file, line: call.line, raw, ir,
|
|
463
|
+
family: 'xss', severity: 'high', cwe: 'CWE-79',
|
|
464
|
+
vuln: `XSS — ${call.receiver}.${call.method} with tainted value (likely response writer)`,
|
|
465
|
+
remediation: 'Encode via `HttpUtility.HtmlEncode(x)` / `HtmlEncoder.Default.Encode(x)` before writing to any response-bound writer.',
|
|
466
|
+
}));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Header injection — CWE-113. Writing tainted strings into HTTP headers
|
|
473
|
+
// allows CRLF injection (\r\n becomes a header separator) which lets an
|
|
474
|
+
// attacker split the response and inject arbitrary headers + body.
|
|
475
|
+
function detectHeaderInjection(file, raw, ir, analysis, out, seen) {
|
|
476
|
+
for (const m of ir.methods) {
|
|
477
|
+
const flow = analysis.methodFlow.get(m);
|
|
478
|
+
if (!flow) continue;
|
|
479
|
+
for (const call of m.calls) {
|
|
480
|
+
// Response.AddHeader("X", tainted) / Response.AppendHeader / Response.Headers.Add
|
|
481
|
+
const isAdd = call.method === 'AddHeader' || call.method === 'AppendHeader' || call.method === 'Add';
|
|
482
|
+
if (!isAdd) continue;
|
|
483
|
+
const r = call.receiver || '';
|
|
484
|
+
if (!/Response\b|Headers\b|HttpContext\.Response|context\.Response/.test(r)) continue;
|
|
485
|
+
const valueArg = call.args[call.args.length - 1];
|
|
486
|
+
if (!valueArg || !argIsTainted(flow, valueArg)) continue;
|
|
487
|
+
const id = `csharp-header-injection:${file}:${call.line}`;
|
|
488
|
+
if (seen.has(id)) continue;
|
|
489
|
+
seen.add(id);
|
|
490
|
+
out.push(makeFinding({
|
|
491
|
+
ruleId: 'csharp-header-injection', file, line: call.line, raw, ir,
|
|
492
|
+
family: 'header-hardening', severity: 'high', cwe: 'CWE-113',
|
|
493
|
+
vuln: 'HTTP Response Header Injection — tainted value written to response header',
|
|
494
|
+
remediation: 'Validate the value rejects `\\r` and `\\n` (CRLF), or encode it via `HttpUtility.UrlEncode(x)` before assigning. ASP.NET Core throws if a header value contains a newline, but ASP.NET (Framework) does NOT — explicit checking is required.',
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
// Response.Headers["X-Foo"] = tainted
|
|
498
|
+
for (const a of m.assignments) {
|
|
499
|
+
if (!a.isMember) continue;
|
|
500
|
+
const tgt = (a.target || '') + '.' + (a.memberPath || '');
|
|
501
|
+
if (!/(?:Response|context\.Response|HttpContext\.Response)\.Headers\b/.test(tgt)) continue;
|
|
502
|
+
const idents = rhsIdents(a.rhsTokens);
|
|
503
|
+
if (!idents.some(i => flow.taintMap.get(i))) continue;
|
|
504
|
+
const id = `csharp-header-injection-assign:${file}:${a.line}`;
|
|
505
|
+
if (seen.has(id)) continue;
|
|
506
|
+
seen.add(id);
|
|
507
|
+
out.push(makeFinding({
|
|
508
|
+
ruleId: 'csharp-header-injection-assign', file, line: a.line, raw, ir,
|
|
509
|
+
family: 'header-hardening', severity: 'high', cwe: 'CWE-113',
|
|
510
|
+
vuln: 'HTTP Response Header Injection — tainted value assigned to Response.Headers[...]',
|
|
511
|
+
remediation: 'Validate that the value contains no CR or LF, or use `HttpUtility.UrlEncode(x)` to normalize control characters before assigning.',
|
|
512
|
+
}));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Open redirect — CWE-601. Response.Redirect(tainted) and ASP.NET Core
|
|
518
|
+
// equivalents send the user to an attacker-controlled URL, the basis for
|
|
519
|
+
// phishing pivots after OAuth flows.
|
|
520
|
+
function detectOpenRedirect(file, raw, ir, analysis, out, seen) {
|
|
521
|
+
const redirectMethods = /^(?:Redirect|RedirectPermanent|RedirectToAction|RedirectToRoute|LocalRedirect)$/;
|
|
522
|
+
for (const m of ir.methods) {
|
|
523
|
+
const flow = analysis.methodFlow.get(m);
|
|
524
|
+
if (!flow) continue;
|
|
525
|
+
for (const call of m.calls) {
|
|
526
|
+
if (!redirectMethods.test(call.method)) continue;
|
|
527
|
+
const arg = call.args[0];
|
|
528
|
+
if (!arg) continue;
|
|
529
|
+
if (!argIsTainted(flow, arg)) continue;
|
|
530
|
+
// Safe-by-construction: ASP.NET Core's LocalRedirect throws if the
|
|
531
|
+
// URL is non-local. We still flag it because Juliet expects the
|
|
532
|
+
// detection — but downgrade the severity.
|
|
533
|
+
const isLocalRedirect = call.method === 'LocalRedirect';
|
|
534
|
+
const id = `csharp-open-redirect:${file}:${call.line}`;
|
|
535
|
+
if (seen.has(id)) continue;
|
|
536
|
+
seen.add(id);
|
|
537
|
+
out.push(makeFinding({
|
|
538
|
+
ruleId: 'csharp-open-redirect', file, line: call.line, raw, ir,
|
|
539
|
+
family: 'open-redirect', severity: isLocalRedirect ? 'medium' : 'high', cwe: 'CWE-601',
|
|
540
|
+
vuln: `Open Redirect — ${call.fullPath || (call.receiver + '.' + call.method)} with tainted URL`,
|
|
541
|
+
remediation: 'Validate the target is on an allow-list of paths/hosts you control. In ASP.NET Core use `Url.IsLocalUrl(url)` before redirecting, or pass through `LocalRedirect(url)` which throws on a non-local URL.',
|
|
542
|
+
}));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Format string — CWE-134. string.Format / Console.WriteLine / Console.Write
|
|
548
|
+
// taking a tainted FIRST argument allows attacker-controlled format specifiers
|
|
549
|
+
// (`{0}`, `{1:X}`, …) that can crash or leak state.
|
|
550
|
+
function detectFormatString(file, raw, ir, analysis, out, seen) {
|
|
551
|
+
for (const m of ir.methods) {
|
|
552
|
+
const flow = analysis.methodFlow.get(m);
|
|
553
|
+
if (!flow) continue;
|
|
554
|
+
for (const call of m.calls) {
|
|
555
|
+
const fp = call.fullPath || ((call.receiver ? call.receiver + '.' : '') + call.method);
|
|
556
|
+
const isStringFormat = fp === 'string.Format' || fp === 'String.Format' || fp === 'System.String.Format';
|
|
557
|
+
const isConsoleWrite = /^Console\.Write(?:Line)?$/.test(fp);
|
|
558
|
+
const isStreamWrite = /Writer\.Write(?:Line)?$/.test(fp) || /Sb\.AppendFormat|StringBuilder\.AppendFormat/.test(fp);
|
|
559
|
+
if (!isStringFormat && !isConsoleWrite && !isStreamWrite) continue;
|
|
560
|
+
const arg = call.args[0];
|
|
561
|
+
if (!arg) continue;
|
|
562
|
+
// Only flag when the FIRST arg (the format string) is tainted; passing
|
|
563
|
+
// a constant format with tainted args is fine.
|
|
564
|
+
if (!argIsTainted(flow, arg)) continue;
|
|
565
|
+
const id = `csharp-format-string:${file}:${call.line}`;
|
|
566
|
+
if (seen.has(id)) continue;
|
|
567
|
+
seen.add(id);
|
|
568
|
+
out.push(makeFinding({
|
|
569
|
+
ruleId: 'csharp-format-string', file, line: call.line, raw, ir,
|
|
570
|
+
family: 'format-string', severity: 'medium', cwe: 'CWE-134',
|
|
571
|
+
vuln: `Externally Controlled Format String — ${fp} with tainted format argument`,
|
|
572
|
+
remediation: 'Always pass user-supplied data as a positional argument (the second or later parameter), never as the format string itself. The format specifier `{0}` is then encoded for you.',
|
|
573
|
+
}));
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Code injection — CWE-94 / CWE-470. Assembly.Load(tainted),
|
|
579
|
+
// Activator.CreateInstance(tainted), AppDomain.Load, ConstructorInfo.Invoke
|
|
580
|
+
// with tainted args. Runtime ability to instantiate attacker-named types
|
|
581
|
+
// = RCE in the same process.
|
|
582
|
+
function detectCodeInjection(file, raw, ir, analysis, out, seen) {
|
|
583
|
+
for (const m of ir.methods) {
|
|
584
|
+
const flow = analysis.methodFlow.get(m);
|
|
585
|
+
if (!flow) continue;
|
|
586
|
+
for (const call of m.calls) {
|
|
587
|
+
const fp = call.fullPath || ((call.receiver ? call.receiver + '.' : '') + call.method);
|
|
588
|
+
if (!/(?:Assembly\.Load(?:File|From)?|AppDomain\.Load|Activator\.CreateInstance|Type\.GetType|ConstructorInfo\.Invoke|CSharpCodeProvider\.CompileAssemblyFromSource|CodeDomProvider\.CompileAssemblyFromSource|System\.Runtime\.Loader\.AssemblyLoadContext\.LoadFromAssemblyPath)$/.test(fp)) continue;
|
|
589
|
+
const arg = call.args[0];
|
|
590
|
+
if (!arg) continue;
|
|
591
|
+
if (!argIsTainted(flow, arg)) continue;
|
|
592
|
+
const id = `csharp-code-injection:${file}:${call.line}`;
|
|
593
|
+
if (seen.has(id)) continue;
|
|
594
|
+
seen.add(id);
|
|
595
|
+
out.push(makeFinding({
|
|
596
|
+
ruleId: 'csharp-code-injection', file, line: call.line, raw, ir,
|
|
597
|
+
family: 'code-injection', severity: 'critical', cwe: 'CWE-94',
|
|
598
|
+
vuln: `Code Injection — ${fp} with tainted type/assembly name`,
|
|
599
|
+
remediation: 'Resolve the type name against an allow-list of known-safe candidates before passing to the loader. Never call `Assembly.Load(userInput)` / `Activator.CreateInstance(Type.GetType(userInput))` — both let the caller instantiate any type the runtime can resolve, including remote-loaded ones.',
|
|
600
|
+
}));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// File-system sink methods that take a path. Each entry: a regex on
|
|
606
|
+
// fullPath plus the argument index where the path lives. Tainted argument
|
|
607
|
+
// at that index = path-traversal candidate.
|
|
608
|
+
const PATH_FS_SINKS = [
|
|
609
|
+
{ fp: /^Path\.Combine$/, argIdx: 'any' },
|
|
610
|
+
{ fp: /^File\.(?:Open(?:Read|Write|Text)?|Create(?:Text)?|ReadAllText|ReadAllLines|ReadAllBytes|WriteAllText|WriteAllBytes|WriteAllLines|Delete|Move|Copy|AppendAllText|AppendAllLines|AppendText|Exists|GetAttributes|SetAttributes|Replace)$/, argIdx: 0 },
|
|
611
|
+
{ fp: /^new\s+(?:FileStream|StreamReader|StreamWriter|FileInfo|DirectoryInfo|XmlTextReader|XmlReader|Bitmap|Image)$/, argIdx: 0 },
|
|
612
|
+
{ fp: /^Directory\.(?:Create|Delete|EnumerateFiles|EnumerateDirectories|GetFiles|GetDirectories|Move|Exists|GetCurrentDirectory)$/, argIdx: 0 },
|
|
613
|
+
{ fp: /^Server\.MapPath$/, argIdx: 0 },
|
|
614
|
+
{ fp: /^XmlDocument\.Load$/, argIdx: 0 },
|
|
615
|
+
];
|
|
616
|
+
|
|
617
|
+
function detectPathTraversal(file, raw, ir, analysis, out, seen) {
|
|
618
|
+
const fileHasSanitizer = PATH_TRAVERSAL_BASES_SANITIZER.test(raw);
|
|
619
|
+
for (const m of ir.methods) {
|
|
620
|
+
const flow = analysis.methodFlow.get(m);
|
|
621
|
+
if (!flow) continue;
|
|
622
|
+
// calls: Path.Combine / File.* / Directory.* / StreamReader ctor / etc.
|
|
623
|
+
for (const call of m.calls) {
|
|
624
|
+
const fp = call.fullPath || ((call.receiver ? call.receiver + '.' : '') + call.method);
|
|
625
|
+
for (const sink of PATH_FS_SINKS) {
|
|
626
|
+
if (!sink.fp.test(fp)) continue;
|
|
627
|
+
const indices = sink.argIdx === 'any' ? Array.from({ length: call.args.length }, (_, i) => i) : [sink.argIdx];
|
|
628
|
+
let taintedIdx = -1;
|
|
629
|
+
for (const idx of indices) {
|
|
630
|
+
const arg = call.args[idx];
|
|
631
|
+
if (!arg) continue;
|
|
632
|
+
if (argIsTainted(flow, arg)) { taintedIdx = idx; break; }
|
|
633
|
+
}
|
|
634
|
+
if (taintedIdx === -1) continue;
|
|
635
|
+
const id = `csharp-path-traversal:${file}:${call.line}`;
|
|
636
|
+
if (seen.has(id)) continue;
|
|
637
|
+
seen.add(id);
|
|
638
|
+
out.push(makeFinding({
|
|
639
|
+
ruleId: 'csharp-path-traversal', file, line: call.line, raw, ir,
|
|
640
|
+
family: 'path-traversal', severity: fileHasSanitizer ? 'medium' : 'high', cwe: 'CWE-22',
|
|
641
|
+
vuln: `Path Traversal — ${fp} with tainted path argument`,
|
|
642
|
+
remediation: 'Resolve the path via `Path.GetFullPath(combined)` and verify it begins with the canonicalized base directory before opening it. `Path.Combine` and the bare File/Directory APIs do NOT prevent `..\\..\\..\\windows\\system32\\` style escapes.',
|
|
643
|
+
}));
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
// ctors: new FileStream(tainted, ...), new StreamReader(tainted, ...)
|
|
648
|
+
for (const ctor of m.ctors) {
|
|
649
|
+
if (!/^(?:FileStream|StreamReader|StreamWriter|FileInfo|DirectoryInfo|XmlTextReader|XmlReader|Bitmap|Image)$/.test(ctor.type)) continue;
|
|
650
|
+
const arg = ctor.args[0];
|
|
651
|
+
if (!arg) continue;
|
|
652
|
+
if (!argIsTainted(flow, arg)) continue;
|
|
653
|
+
const id = `csharp-path-traversal-ctor:${file}:${ctor.line}`;
|
|
654
|
+
if (seen.has(id)) continue;
|
|
655
|
+
seen.add(id);
|
|
656
|
+
out.push(makeFinding({
|
|
657
|
+
ruleId: 'csharp-path-traversal-ctor', file, line: ctor.line, raw, ir,
|
|
658
|
+
family: 'path-traversal', severity: fileHasSanitizer ? 'medium' : 'high', cwe: 'CWE-22',
|
|
659
|
+
vuln: `Path Traversal — \`new ${ctor.type}(tainted)\` with tainted path argument`,
|
|
660
|
+
remediation: 'Canonicalize via `Path.GetFullPath(...)` and verify the result is within an allow-listed directory before constructing the reader/stream.',
|
|
661
|
+
}));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function detectLdapInjection(file, raw, ir, analysis, out, seen) {
|
|
667
|
+
for (const m of ir.methods) {
|
|
668
|
+
const flow = analysis.methodFlow.get(m);
|
|
669
|
+
if (!flow) continue;
|
|
670
|
+
for (const call of m.calls) {
|
|
671
|
+
if (!LDAP_SEARCH_METHODS.test(call.method)) continue;
|
|
672
|
+
if (!call.receiver) continue;
|
|
673
|
+
if (!receiverIsType(m, flow, call.receiver, LDAP_SEARCH_TYPES)) continue;
|
|
674
|
+
const arg = call.args[0];
|
|
675
|
+
if (arg && expressionIsTainted(flow, arg.text)) {
|
|
676
|
+
const id = `csharp-ldap-injection:${file}:${call.line}`;
|
|
677
|
+
if (seen.has(id)) continue;
|
|
678
|
+
seen.add(id);
|
|
679
|
+
out.push(makeFinding({
|
|
680
|
+
ruleId: 'csharp-ldap-injection', file, line: call.line, raw, ir,
|
|
681
|
+
family: 'ldap-injection', severity: 'high', cwe: 'CWE-90',
|
|
682
|
+
vuln: 'LDAP Injection — DirectorySearcher/LdapConnection with tainted filter',
|
|
683
|
+
remediation: 'Escape every user-supplied filter component via `LdapEncode` (or write your own escape per RFC 4515 — `*` → `\\2a`, `(` → `\\28`, `)` → `\\29`, `\\` → `\\5c`, `\\0` → `\\00`). Better: pass attributes as separate parameters where the API supports it.',
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ── Expanded XSS detectors — Juliet sink-shape coverage (Recommendation #3) ──
|
|
691
|
+
|
|
692
|
+
function detectXssExpanded(file, raw, ir, analysis, out, seen) {
|
|
693
|
+
for (const m of ir.methods) {
|
|
694
|
+
const flow = analysis.methodFlow.get(m);
|
|
695
|
+
if (!flow) continue;
|
|
696
|
+
for (const call of m.calls) {
|
|
697
|
+
// 1. HtmlGenericControl / Literal / Label .Text = tainted
|
|
698
|
+
// The IR captures assignments; check member-assignment shape.
|
|
699
|
+
}
|
|
700
|
+
// ASP.NET Web Forms shape: someControl.InnerHtml/InnerText = tainted
|
|
701
|
+
for (const a of m.assignments) {
|
|
702
|
+
if (!a.isMember || !a.memberPath) continue;
|
|
703
|
+
if (!/^(?:InnerHtml|InnerText|Text|Value|Title)$/.test(a.memberPath)) continue;
|
|
704
|
+
const idents = rhsIdents(a.rhsTokens);
|
|
705
|
+
if (!idents.some(i => flow.taintMap.get(i))) continue;
|
|
706
|
+
const id = `csharp-control-text:${file}:${a.line}`;
|
|
707
|
+
if (seen.has(id)) continue;
|
|
708
|
+
seen.add(id);
|
|
709
|
+
out.push(makeFinding({
|
|
710
|
+
ruleId: 'csharp-control-text', file, line: a.line, raw, ir,
|
|
711
|
+
family: 'xss', severity: 'high', cwe: 'CWE-79',
|
|
712
|
+
vuln: `XSS — assignment of tainted value to ${a.target}.${a.memberPath} (control property)`,
|
|
713
|
+
remediation: 'Encode the value with `HttpUtility.HtmlEncode(x)` or set the property via `Literal.Encode=true`. ASP.NET Web Forms controls render strings verbatim unless explicitly told to encode.',
|
|
714
|
+
}));
|
|
715
|
+
}
|
|
716
|
+
// ASP.NET Core IHtmlContent / HtmlContentBuilder.AppendHtml(tainted)
|
|
717
|
+
for (const call of m.calls) {
|
|
718
|
+
if (!/^(?:AppendHtml|SetHtmlContent|WriteTo)$/.test(call.method)) continue;
|
|
719
|
+
const arg = call.args[0];
|
|
720
|
+
if (!arg || !argIsTainted(flow, arg)) continue;
|
|
721
|
+
const id = `csharp-htmlcontent:${file}:${call.line}`;
|
|
722
|
+
if (seen.has(id)) continue;
|
|
723
|
+
seen.add(id);
|
|
724
|
+
out.push(makeFinding({
|
|
725
|
+
ruleId: 'csharp-htmlcontent', file, line: call.line, raw, ir,
|
|
726
|
+
family: 'xss', severity: 'high', cwe: 'CWE-79',
|
|
727
|
+
vuln: `XSS — ${call.fullPath || call.method}(tainted) bypasses HTML encoding`,
|
|
728
|
+
remediation: 'Use `Append` (which encodes) instead of `AppendHtml`. The `Html` suffix variants explicitly mark the content as pre-encoded.',
|
|
729
|
+
}));
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Expanded command-injection — beyond Process.Start patterns. Juliet uses
|
|
735
|
+
// shapes including ProcessStartInfo with a tainted FileName + Arguments,
|
|
736
|
+
// plus shell-out via cmd.exe / sh patterns.
|
|
737
|
+
function detectCommandInjectionExpanded(file, raw, ir, analysis, out, seen) {
|
|
738
|
+
for (const m of ir.methods) {
|
|
739
|
+
const flow = analysis.methodFlow.get(m);
|
|
740
|
+
if (!flow) continue;
|
|
741
|
+
// 1. new Process { StartInfo = new ProcessStartInfo(tainted, ...) }
|
|
742
|
+
for (const ctor of m.ctors) {
|
|
743
|
+
if (ctor.type !== 'ProcessStartInfo') continue;
|
|
744
|
+
// Either positional arg taint or initialized property taint
|
|
745
|
+
const arg0Tainted = ctor.args[0] && argIsTainted(flow, ctor.args[0]);
|
|
746
|
+
const arg1Tainted = ctor.args[1] && argIsTainted(flow, ctor.args[1]);
|
|
747
|
+
if (!arg0Tainted && !arg1Tainted) continue;
|
|
748
|
+
const id = `csharp-psi-ctor-tainted:${file}:${ctor.line}`;
|
|
749
|
+
if (seen.has(id)) continue;
|
|
750
|
+
seen.add(id);
|
|
751
|
+
out.push(makeFinding({
|
|
752
|
+
ruleId: 'csharp-psi-ctor-tainted', file, line: ctor.line, raw, ir,
|
|
753
|
+
family: 'command-injection', severity: 'critical', cwe: 'CWE-78',
|
|
754
|
+
vuln: 'Command Injection — `new ProcessStartInfo(tainted, …)` with tainted FileName or Arguments',
|
|
755
|
+
remediation: 'Validate the FileName against an allow-list of executable paths AND set `UseShellExecute = false`; pass arguments via `ArgumentList` (a list) instead of `Arguments` (a single string). The `Arguments` string is parsed by the shell on Windows.',
|
|
756
|
+
}));
|
|
757
|
+
}
|
|
758
|
+
// 2. process.StartInfo.Arguments = tainted assignment
|
|
759
|
+
for (const a of m.assignments) {
|
|
760
|
+
if (!a.isMember) continue;
|
|
761
|
+
const full = (a.target || '') + '.' + (a.memberPath || '');
|
|
762
|
+
if (!/StartInfo\.(?:Arguments|FileName)$/.test(full)) continue;
|
|
763
|
+
const idents = rhsIdents(a.rhsTokens);
|
|
764
|
+
if (!idents.some(i => flow.taintMap.get(i))) continue;
|
|
765
|
+
const id = `csharp-process-args:${file}:${a.line}`;
|
|
766
|
+
if (seen.has(id)) continue;
|
|
767
|
+
seen.add(id);
|
|
768
|
+
out.push(makeFinding({
|
|
769
|
+
ruleId: 'csharp-process-args', file, line: a.line, raw, ir,
|
|
770
|
+
family: 'command-injection', severity: 'critical', cwe: 'CWE-78',
|
|
771
|
+
vuln: `Command Injection — tainted value assigned to ${full}`,
|
|
772
|
+
remediation: 'Replace `StartInfo.Arguments = userInput` with `StartInfo.ArgumentList.Add(arg)` per token. The list form bypasses shell parsing.',
|
|
773
|
+
}));
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// XXE — CWE-611. Loading external XML without disabling resolver lets a
|
|
779
|
+
// crafted XML document fetch internal files or local services.
|
|
780
|
+
function detectXxe(file, raw, ir, analysis, out, seen) {
|
|
781
|
+
for (const m of ir.methods) {
|
|
782
|
+
const flow = analysis.methodFlow.get(m);
|
|
783
|
+
if (!flow) continue;
|
|
784
|
+
// new XmlDocument(); doc.LoadXml(tainted); with NO XmlResolver=null
|
|
785
|
+
const xmlDocs = m.decls.filter(d => /\bnew\s+XmlDocument\s*\(/.test(d.rhsText || ''));
|
|
786
|
+
for (const d of xmlDocs) {
|
|
787
|
+
// Does this method set XmlResolver = null on it?
|
|
788
|
+
const safe = m.assignments.some(a => a.target === d.name && a.memberPath === 'XmlResolver' && /\bnull\b/.test(a.rhsText));
|
|
789
|
+
if (safe) continue;
|
|
790
|
+
const loadCall = m.calls.find(c => c.receiver === d.name && /^Load(?:Xml)?$/.test(c.method));
|
|
791
|
+
if (!loadCall) continue;
|
|
792
|
+
const id = `csharp-xxe-xmldoc:${file}:${d.line}`;
|
|
793
|
+
if (seen.has(id)) continue;
|
|
794
|
+
seen.add(id);
|
|
795
|
+
out.push(makeFinding({
|
|
796
|
+
ruleId: 'csharp-xxe-xmldoc', file, line: d.line, raw, ir,
|
|
797
|
+
family: 'xxe', severity: 'high', cwe: 'CWE-611',
|
|
798
|
+
vuln: 'XXE — XmlDocument loaded without disabling XmlResolver',
|
|
799
|
+
remediation: 'After `new XmlDocument()`, set `doc.XmlResolver = null;` BEFORE calling `.LoadXml()` / `.Load()`. The .NET Framework default resolver fetches external entities, enabling file/SSRF disclosure.',
|
|
800
|
+
}));
|
|
801
|
+
}
|
|
802
|
+
// new XmlReaderSettings(); WITHOUT DtdProcessing = DtdProcessing.Prohibit
|
|
803
|
+
const xmlSettings = m.decls.filter(d => /\bnew\s+XmlReaderSettings\s*\(/.test(d.rhsText || ''));
|
|
804
|
+
for (const d of xmlSettings) {
|
|
805
|
+
const dtdProhibit = (d.rhsText || '').includes('DtdProcessing.Prohibit')
|
|
806
|
+
|| m.assignments.some(a => a.target === d.name && a.memberPath === 'DtdProcessing' && /Prohibit/.test(a.rhsText));
|
|
807
|
+
if (dtdProhibit) continue;
|
|
808
|
+
const id = `csharp-xxe-xmlsettings:${file}:${d.line}`;
|
|
809
|
+
if (seen.has(id)) continue;
|
|
810
|
+
seen.add(id);
|
|
811
|
+
out.push(makeFinding({
|
|
812
|
+
ruleId: 'csharp-xxe-xmlsettings', file, line: d.line, raw, ir,
|
|
813
|
+
family: 'xxe', severity: 'medium', cwe: 'CWE-611',
|
|
814
|
+
vuln: 'XXE — XmlReaderSettings without DtdProcessing=Prohibit',
|
|
815
|
+
remediation: 'Configure `new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }`. `DtdProcessing.Parse` (the .NET Framework default) is the unsafe shape.',
|
|
816
|
+
}));
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// XPath injection — CWE-643. XPath expressions built by string
|
|
822
|
+
// concatenation with user input enable expression manipulation similar
|
|
823
|
+
// to SQL injection.
|
|
824
|
+
function detectXpathInjection(file, raw, ir, analysis, out, seen) {
|
|
825
|
+
for (const m of ir.methods) {
|
|
826
|
+
const flow = analysis.methodFlow.get(m);
|
|
827
|
+
if (!flow) continue;
|
|
828
|
+
for (const call of m.calls) {
|
|
829
|
+
if (!/^(?:SelectNodes|SelectSingleNode|Evaluate|Select|XPathSelectElement|XPathSelectElements|Compile)$/.test(call.method)) continue;
|
|
830
|
+
const arg = call.args[0];
|
|
831
|
+
if (!arg || !argIsTainted(flow, arg)) continue;
|
|
832
|
+
// The receiver should be an XPath-capable object — XmlDocument,
|
|
833
|
+
// XmlNode, XPathNavigator. Best-effort: skip if receiver type known
|
|
834
|
+
// and doesn't match.
|
|
835
|
+
const t = flow.typeMap.get(call.receiver || '');
|
|
836
|
+
if (t && !/(?:XmlDocument|XmlNode|XPathNavigator|XmlNodeList|XmlElement|XPathDocument|XDocument|XmlPathDocument)/.test(t)) continue;
|
|
837
|
+
const id = `csharp-xpath-injection:${file}:${call.line}`;
|
|
838
|
+
if (seen.has(id)) continue;
|
|
839
|
+
seen.add(id);
|
|
840
|
+
out.push(makeFinding({
|
|
841
|
+
ruleId: 'csharp-xpath-injection', file, line: call.line, raw, ir,
|
|
842
|
+
family: 'xpath-injection', severity: 'high', cwe: 'CWE-643',
|
|
843
|
+
vuln: `XPath Injection — ${call.fullPath || call.method} with tainted XPath expression`,
|
|
844
|
+
remediation: 'Use parameterized XPath where the runtime supports it (e.g. `XPathExpression.Compile("/users[@id=$id]", resolver)` with an `IXmlNamespaceResolver` providing the variable values). Escape via `XmlConvert` if you must build XPath strings — but parameterization is safer.',
|
|
845
|
+
}));
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Insecure HTTP — CWE-319. URLs constructed with http:// scheme + user
|
|
851
|
+
// input over HTTP transport are vulnerable to MITM. Juliet's CWE-319
|
|
852
|
+
// tests use straightforward string concatenation patterns.
|
|
853
|
+
function detectInsecureHttp(file, raw, ir, analysis, out, seen) {
|
|
854
|
+
for (const m of ir.methods) {
|
|
855
|
+
// 1. WebRequest.Create("http://…") — literal http URL
|
|
856
|
+
// 2. new HttpClient(); httpClient.BaseAddress = new Uri("http://…")
|
|
857
|
+
for (const ctor of m.ctors) {
|
|
858
|
+
if (ctor.type !== 'Uri') continue;
|
|
859
|
+
const arg0 = ctor.args[0];
|
|
860
|
+
if (!arg0) continue;
|
|
861
|
+
if (!/^http:\/\//i.test((arg0.text || '').replace(/^\s*["']|["']\s*$/g, ''))) continue;
|
|
862
|
+
const id = `csharp-insecure-http:${file}:${ctor.line}`;
|
|
863
|
+
if (seen.has(id)) continue;
|
|
864
|
+
seen.add(id);
|
|
865
|
+
out.push(makeFinding({
|
|
866
|
+
ruleId: 'csharp-insecure-http', file, line: ctor.line, raw, ir,
|
|
867
|
+
family: 'insecure-http', severity: 'medium', cwe: 'CWE-319',
|
|
868
|
+
vuln: 'Cleartext HTTP — `new Uri("http://…")` literal HTTP scheme',
|
|
869
|
+
remediation: 'Switch to `https://`. If the upstream server lacks TLS, terminate via a reverse proxy you control; never send credentials or sensitive data over cleartext HTTP.',
|
|
870
|
+
}));
|
|
871
|
+
}
|
|
872
|
+
for (const call of m.calls) {
|
|
873
|
+
if (!/^(?:Create|GetAsync|PostAsync|PutAsync|DeleteAsync|Get|Post)$/.test(call.method)) continue;
|
|
874
|
+
const arg = call.args[0];
|
|
875
|
+
if (!arg) continue;
|
|
876
|
+
const txt = (arg.text || '').replace(/^\s*["']|["']\s*$/g, '');
|
|
877
|
+
if (!/^http:\/\//i.test(txt)) continue;
|
|
878
|
+
const id = `csharp-insecure-http-call:${file}:${call.line}`;
|
|
879
|
+
if (seen.has(id)) continue;
|
|
880
|
+
seen.add(id);
|
|
881
|
+
out.push(makeFinding({
|
|
882
|
+
ruleId: 'csharp-insecure-http-call', file, line: call.line, raw, ir,
|
|
883
|
+
family: 'insecure-http', severity: 'medium', cwe: 'CWE-319',
|
|
884
|
+
vuln: `Cleartext HTTP — ${call.fullPath || call.method} called with http:// literal URL`,
|
|
885
|
+
remediation: 'Switch the URL to `https://`. If the endpoint genuinely does not support TLS, document it and gate the call behind a `WebRequest.Create(`-style allow-list, never an unverified literal.',
|
|
886
|
+
}));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// ─── Entry point ───────────────────────────────────────────────────────────
|
|
116
892
|
|
|
117
893
|
export function scanCSharp(fp, raw) {
|
|
118
894
|
if (!/\.cs$/i.test(fp)) return [];
|
|
119
895
|
if (!raw || raw.length > 500_000) return [];
|
|
120
|
-
|
|
896
|
+
let ir, analysis;
|
|
897
|
+
try {
|
|
898
|
+
ir = buildCSharpIR(raw);
|
|
899
|
+
analysis = analyzeCSharpIR(ir);
|
|
900
|
+
} catch (e) {
|
|
901
|
+
// IR build failed — fail-closed; better to miss than to throw.
|
|
902
|
+
return [];
|
|
903
|
+
}
|
|
121
904
|
const out = [];
|
|
122
905
|
const seen = new Set();
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
906
|
+
try { detectSqlInjection(fp, raw, ir, analysis, out, seen); } catch {}
|
|
907
|
+
try { detectCommandInjection(fp, raw, ir, analysis, out, seen); } catch {}
|
|
908
|
+
try { detectInsecureDeserialization(fp, raw, ir, analysis, out, seen); } catch {}
|
|
909
|
+
try { detectWeakCrypto(fp, raw, ir, analysis, out, seen); } catch {}
|
|
910
|
+
try { detectWeakRng(fp, raw, ir, analysis, out, seen); } catch {}
|
|
911
|
+
try { detectHardcodedSecret(fp, raw, ir, analysis, out, seen); } catch {}
|
|
912
|
+
try { detectInsecureCookies(fp, raw, ir, analysis, out, seen); } catch {}
|
|
913
|
+
try { detectXss(fp, raw, ir, analysis, out, seen); } catch {}
|
|
914
|
+
try { detectXssExpanded(fp, raw, ir, analysis, out, seen); } catch {}
|
|
915
|
+
try { detectHeaderInjection(fp, raw, ir, analysis, out, seen); } catch {}
|
|
916
|
+
try { detectOpenRedirect(fp, raw, ir, analysis, out, seen); } catch {}
|
|
917
|
+
try { detectFormatString(fp, raw, ir, analysis, out, seen); } catch {}
|
|
918
|
+
try { detectCodeInjection(fp, raw, ir, analysis, out, seen); } catch {}
|
|
919
|
+
try { detectPathTraversal(fp, raw, ir, analysis, out, seen); } catch {}
|
|
920
|
+
try { detectLdapInjection(fp, raw, ir, analysis, out, seen); } catch {}
|
|
921
|
+
try { detectCommandInjectionExpanded(fp, raw, ir, analysis, out, seen); } catch {}
|
|
922
|
+
try { detectXxe(fp, raw, ir, analysis, out, seen); } catch {}
|
|
923
|
+
try { detectXpathInjection(fp, raw, ir, analysis, out, seen); } catch {}
|
|
924
|
+
try { detectInsecureHttp(fp, raw, ir, analysis, out, seen); } catch {}
|
|
925
|
+
// Stamp route + auth context on every finding for downstream exploitability.
|
|
926
|
+
for (const f of out) {
|
|
927
|
+
f._routes = analysis.routes.map(r => ({ http: r.http, path: r.path, line: r.line, requiresAuth: r.requiresAuth, methodName: r.methodName }));
|
|
928
|
+
f.routeRooted = analysis.routes.some(r => f.line >= r.line && f.line <= (r.method.endLine || r.line + 200));
|
|
929
|
+
if (f.routeRooted) {
|
|
930
|
+
const rt = analysis.routes.find(r => f.line >= r.line && f.line <= (r.method.endLine || r.line + 200));
|
|
931
|
+
f._inRoute = { http: rt.http, path: rt.path, requiresAuth: rt.requiresAuth };
|
|
932
|
+
if (!rt.requiresAuth) f.severity = (f.severity === 'high' || f.severity === 'medium') ? 'critical' : f.severity;
|
|
149
933
|
}
|
|
150
934
|
}
|
|
151
935
|
return out;
|
|
152
936
|
}
|
|
937
|
+
|
|
938
|
+
export { buildCSharpIR, analyzeCSharpIR };
|