Skip to content

Commit b3dc20e

Browse files
authored
fix(rules): improve precision of 4 high-FP dotnet opengrep rules (#63)
* fix(rules): improve precision of 4 high-FP dotnet opengrep rules Addresses customer SAST evaluation feedback where 4 rules produced 150/170 false positives (88% of all FPs), inflating the reported FP rate to 91%. Rules fixed: - dotnet-xss-response-write: Convert to taint mode. Previously matched any .Write() call including Serilog ITextFormatter log sinks. Now requires data flow from user input sources to Response.Write sinks. - dotnet-hardcoded-credentials: Add value inspection and credential API patterns. Previously matched on variable names alone, flagging config key paths like "UseCaptchaOnResetPassword". - dotnet-crypto-failures: Target actual weak algorithms (3DES, DES, RC2, RijndaelManaged) instead of Encoding.UTF8.GetBytes() which flagged the recommended SHA256.HashData(Encoding.UTF8.GetBytes(...)) pattern. - dotnet-path-traversal: Convert to taint mode. Previously matched all Path.Combine() calls including those using framework-provided paths like _env.WebRootPath. Validated with opengrep v1.19.0 against NIST Juliet C# test suite: xss-response-write: Prec 41.6% -> 100%, Recall 47.8% -> 24.3% hardcoded-credentials: Prec 0.0% -> 100%, Recall 0.0% -> 3.6% crypto-failures: Prec 36.7% -> 100%, Recall 51.4% -> 50.0% path-traversal: Prec 0.0% -> 100%, Recall 0.0% -> 45.2% * fix(rules): add ASP.NET Core taint sources and System.IO sinks to dotnet rules Add controller parameter binding sources ([FromQuery], [FromBody], [FromRoute], [FromForm]) and IFormFile.FileName to path-traversal and XSS taint rules. Add Response.WriteAsync and Html.Raw as XSS sinks. Add fully-qualified System.IO.File.* sink variants for ASP.NET Core code that uses explicit namespace qualification. E2E tested against two vulnerable .NET repos: 7 true positives found, zero false positives. * fix(rules): address review feedback on dotnet rule precision - Remove Path.GetFullPath() as path-traversal sanitizer (normalizes but does not prevent traversal on its own) - Broaden hardcoded-credentials variable regex to cover idiomatic C# naming: apiKey, connectionString, privateKey, accessKey, authToken - Remove overly broad Base64 encoding pattern from crypto-failures (benign encoding/transport use generates noise) * fix(rules): correct path-traversal StartsWith sanitizer in dotnet rule The $X.StartsWith($BASE) sanitizer was matching the boolean expression instead of marking the checked path variable as sanitized, so correctly validated paths were still flagged as tainted. Use focus-metavariable + by-side-effect so the sanitizer applies to $X itself. Verified with a synthetic test case: scans of an unsanitized File.ReadAllText still fire, but the same call guarded by full.StartsWith("/var/data/") no longer does. Juliet CWE-23/36 results unchanged at 432 findings (Juliet test cases do not exercise StartsWith validation). opengrep --validate and pytest pass.
1 parent b68e7f8 commit b3dc20e

1 file changed

Lines changed: 202 additions & 31 deletions

File tree

socket_basics/rules/dotnet.yml

Lines changed: 202 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -141,23 +141,37 @@ rules:
141141

142142
# === High Severity Rules ===
143143

144-
# Hardcoded credentials
144+
# Hardcoded credentials - matches both variable-name patterns AND credential API usage
145145
- id: dotnet-hardcoded-credentials
146146
message: "Hard-coded credentials detected. Embedding secrets in source code makes them easily discoverable and impossible to rotate. Use environment variables or a secrets manager instead."
147147
severity: HIGH
148148
languages: [csharp]
149-
patterns:
150-
- pattern-either:
151-
# C#
152-
- pattern: |
153-
private const string $VAR = "...";
154-
- pattern: |
155-
public const string $VAR = "...";
156-
- pattern: |
157-
string $VAR = "...";
158-
- metavariable-regex:
159-
metavariable: $VAR
160-
regex: (?i).*(password|passwd|pwd|secret|token|key|api_key|connection_string).*
149+
pattern-either:
150+
# Pattern 1: Variable name suggests credential AND value is a non-empty literal
151+
- patterns:
152+
- pattern-either:
153+
- pattern: |
154+
private const string $VAR = "$VALUE";
155+
- pattern: |
156+
public const string $VAR = "$VALUE";
157+
- pattern: |
158+
string $VAR = "$VALUE";
159+
- metavariable-regex:
160+
metavariable: $VAR
161+
regex: (?i).*(password|passwd|pwd|secret|secretKey|token|authToken|api_?key|apiKey|accessKey|privateKey|connection_?string|connectionString).*
162+
- metavariable-regex:
163+
metavariable: $VALUE
164+
# Must look like an actual secret: 6+ chars, not a config path or empty
165+
regex: ^(?!$).{6,}
166+
- metavariable-regex:
167+
metavariable: $VALUE
168+
# Exclude config key paths and property names
169+
regex: ^(?!.*\.\w+\.\w+)(?!.*\b(Use|Enable|Disable|Is|Has|Get|Set|On)\w+).*$
170+
# Pattern 2: Credential APIs called with hardcoded string literals
171+
- pattern: new NetworkCredential($USER, "...", ...)
172+
- pattern: new NetworkCredential("...", "...", ...)
173+
- pattern: new SqlConnection("...");
174+
- pattern: new PasswordDeriveBytes("...", ...)
161175
metadata:
162176
category: security
163177
cwe: CWE-798
@@ -223,35 +237,183 @@ rules:
223237
owasp: "A07:2021"
224238
fix: "Never return true from ServerCertificateCustomValidationCallback. Use the default certificate validation from ServicePointManager."
225239

226-
# XSS vulnerabilities
240+
# XSS vulnerabilities - taint mode for accurate user-input tracking
227241
- id: dotnet-xss-response-write
228242
message: "Cross-site scripting (XSS) vulnerability detected. User input is rendered in HTML output without proper escaping, allowing attackers to inject malicious scripts. Sanitize or escape all user input before rendering."
229243
severity: HIGH
230244
languages: [csharp]
231-
pattern-either:
232-
- pattern: Response.Write($USER_INPUT)
233-
- pattern: $RESPONSE.Write($USER_INPUT)
234-
- pattern: HttpContext.Response.Write($USER_INPUT)
245+
mode: taint
246+
pattern-sources:
247+
- pattern-either:
248+
# ASP.NET request input sources
249+
- pattern: Request.QueryString[...]
250+
- pattern: Request.Form[...]
251+
- pattern: Request.Params[...]
252+
- pattern: Request.Cookies[...]
253+
- pattern: Request[$KEY]
254+
- pattern: Request.Headers[...]
255+
- pattern: Request.UserAgent
256+
- pattern: Request.RawUrl
257+
- pattern: Request.Url
258+
- pattern: Request.Path
259+
- pattern: Request.PathInfo
260+
- pattern: (HttpRequest $REQ).QueryString[...]
261+
- pattern: (HttpRequest $REQ).Form[...]
262+
- pattern: (HttpRequest $REQ).Params[...]
263+
- pattern: (HttpRequest $REQ)[$KEY]
264+
# ASP.NET Core model binding
265+
- pattern: $REQ.Query[...]
266+
- pattern: $REQ.Form[...]
267+
- pattern: $REQ.Headers[...]
268+
- pattern: $REQ.Cookies[...]
269+
# ASP.NET Core controller parameter binding
270+
- patterns:
271+
- pattern: $PARAM
272+
- pattern-inside: |
273+
public $RET $METHOD(..., [FromQuery] $TYPE $PARAM, ...) { ... }
274+
- patterns:
275+
- pattern: $PARAM
276+
- pattern-inside: |
277+
public $RET $METHOD(..., [FromBody] $TYPE $PARAM, ...) { ... }
278+
- patterns:
279+
- pattern: $PARAM
280+
- pattern-inside: |
281+
public $RET $METHOD(..., [FromRoute] $TYPE $PARAM, ...) { ... }
282+
- patterns:
283+
- pattern: $PARAM
284+
- pattern-inside: |
285+
public $RET $METHOD(..., [FromForm] $TYPE $PARAM, ...) { ... }
286+
# IFormFile upload sources
287+
- pattern: (IFormFile $F).FileName
288+
- pattern: (IFormFile $F).ContentType
289+
# Network input sources (Juliet-style)
290+
- pattern: (StreamReader $SR).ReadLine()
291+
- pattern: (TextReader $TR).ReadLine()
292+
- pattern: Console.ReadLine()
293+
pattern-propagators:
294+
- pattern: (string $A) + (string $B)
295+
from: $B
296+
to: $A
297+
- pattern: String.Format($FMT, ..., (string $X), ...)
298+
from: $X
299+
to: $FMT
300+
- pattern: String.Concat(..., (string $X), ...)
301+
from: $X
302+
to: String.Concat
303+
pattern-sinks:
304+
- pattern-either:
305+
- pattern: Response.Write(...)
306+
- pattern: Response.WriteAsync(...)
307+
- pattern: HttpContext.Response.Write(...)
308+
- pattern: HttpContext.Response.WriteAsync(...)
309+
# HttpResponse parameter pattern (Juliet, ASP.NET handlers)
310+
- pattern: $RESP.Write(...)
311+
- pattern: $RESP.WriteAsync(...)
312+
# Razor unencoded output
313+
- pattern: Html.Raw(...)
314+
pattern-sanitizers:
315+
- pattern-either:
316+
- pattern: HttpUtility.HtmlEncode(...)
317+
- pattern: HtmlEncoder.Default.Encode(...)
318+
- pattern: WebUtility.HtmlEncode(...)
319+
- pattern: Server.HtmlEncode(...)
320+
- pattern: AntiXssEncoder.HtmlEncode(...)
235321
metadata:
236322
category: security
237323
cwe: CWE-79
238-
confidence: medium
324+
confidence: high
239325
subcategory: xss
240326
vulnerability_class: "Cross-Site Scripting (XSS)"
241327
owasp: "A03:2021"
242328
fix: "Use Razor auto-encoding or HtmlEncoder.Default.Encode(). Never use Html.Raw() with user input. Validate input on both client and server."
243329

244-
# Path traversal
330+
# Path traversal - taint mode for accurate user-input tracking
245331
- id: dotnet-path-traversal
246332
message: "Path traversal vulnerability detected. User input is used in file paths without validation, allowing attackers to access files outside the intended directory. Validate and canonicalize paths before use."
247333
severity: HIGH
248334
languages: [csharp]
249-
pattern-either:
250-
- pattern: File.ReadAllText($PATH + $USER_INPUT)
251-
- pattern: File.ReadAllBytes($PATH + $USER_INPUT)
252-
- pattern: File.WriteAllText($PATH + $USER_INPUT, ...)
253-
- pattern: new FileStream($PATH + $USER_INPUT, ...)
254-
- pattern: Path.Combine($BASE, $USER_INPUT)
335+
mode: taint
336+
pattern-sources:
337+
- pattern-either:
338+
# ASP.NET request sources
339+
- pattern: Request.QueryString[...]
340+
- pattern: Request.Form[...]
341+
- pattern: Request.Params[...]
342+
- pattern: Request[$KEY]
343+
- pattern: Request.Path
344+
- pattern: Request.PathInfo
345+
# ASP.NET Core
346+
- pattern: $REQ.Query[...]
347+
- pattern: $REQ.Form[...]
348+
- pattern: $REQ.RouteValues[...]
349+
# ASP.NET Core controller parameter binding
350+
- patterns:
351+
- pattern: $PARAM
352+
- pattern-inside: |
353+
public $RET $METHOD(..., [FromQuery] $TYPE $PARAM, ...) { ... }
354+
- patterns:
355+
- pattern: $PARAM
356+
- pattern-inside: |
357+
public $RET $METHOD(..., [FromBody] $TYPE $PARAM, ...) { ... }
358+
- patterns:
359+
- pattern: $PARAM
360+
- pattern-inside: |
361+
public $RET $METHOD(..., [FromRoute] $TYPE $PARAM, ...) { ... }
362+
- patterns:
363+
- pattern: $PARAM
364+
- pattern-inside: |
365+
public $RET $METHOD(..., [FromForm] $TYPE $PARAM, ...) { ... }
366+
# IFormFile upload sources
367+
- pattern: (IFormFile $F).FileName
368+
# Network input sources (Juliet-style)
369+
- pattern: (StreamReader $SR).ReadLine()
370+
- pattern: (TextReader $TR).ReadLine()
371+
- pattern: Console.ReadLine()
372+
# Environment and system sources
373+
- pattern: Environment.GetEnvironmentVariable(...)
374+
pattern-propagators:
375+
- pattern: (string $A) + (string $B)
376+
from: $B
377+
to: $A
378+
- pattern: Path.Combine(..., (string $X), ...)
379+
from: $X
380+
to: Path.Combine
381+
- pattern: String.Format($FMT, ..., (string $X), ...)
382+
from: $X
383+
to: $FMT
384+
pattern-sinks:
385+
- pattern-either:
386+
- pattern: File.ReadAllText(...)
387+
- pattern: File.ReadAllBytes(...)
388+
- pattern: File.WriteAllText(...)
389+
- pattern: File.WriteAllBytes(...)
390+
- pattern: File.Open(...)
391+
- pattern: File.OpenRead(...)
392+
- pattern: File.OpenWrite(...)
393+
- pattern: File.Exists(...)
394+
- pattern: File.Delete(...)
395+
# Fully-qualified System.IO variants (common in ASP.NET Core)
396+
- pattern: System.IO.File.ReadAllText(...)
397+
- pattern: System.IO.File.ReadAllBytes(...)
398+
- pattern: System.IO.File.WriteAllText(...)
399+
- pattern: System.IO.File.WriteAllBytes(...)
400+
- pattern: System.IO.File.Exists(...)
401+
- pattern: System.IO.File.Open(...)
402+
- pattern: System.IO.File.Delete(...)
403+
- pattern: new FileStream(...)
404+
- pattern: new StreamReader(...)
405+
- pattern: new StreamWriter(...)
406+
- pattern: Directory.GetFiles(...)
407+
- pattern: Directory.EnumerateFiles(...)
408+
pattern-sanitizers:
409+
- pattern: Path.GetFileName(...)
410+
# Treat the checked path variable as sanitized after a base-path validation.
411+
# focus-metavariable + by-side-effect cleans $X itself, not just the
412+
# StartsWith(...) boolean expression.
413+
- patterns:
414+
- pattern: $X.StartsWith($BASE)
415+
- focus-metavariable: $X
416+
by-side-effect: true
255417
metadata:
256418
category: security
257419
cwe: CWE-22
@@ -623,23 +785,32 @@ rules:
623785
vulnerability_class: "Access Control Violation"
624786
fix: "Review authorization logic for bypass conditions. Use policy-based authorization with IAuthorizationHandler. Test authorization with different user roles."
625787

626-
# A02: Cryptographic Failures
788+
# A02: Cryptographic Failures - targets actual weak algorithm usage
627789
- id: dotnet-crypto-failures
628790
message: "Weak cryptographic algorithm detected. Using broken or outdated algorithms may allow attackers to decrypt data or forge signatures. Use modern algorithms like AES-256, SHA-256, or Ed25519."
629791
severity: HIGH
630792
languages: [csharp]
631793
pattern-either:
632-
- pattern: Encoding.UTF8.GetBytes($PASSWORD)
633-
- pattern: Convert.ToBase64String(Encoding.UTF8.GetBytes($SECRET))
794+
# Weak symmetric ciphers
795+
- pattern: new TripleDESCryptoServiceProvider()
796+
- pattern: new DESCryptoServiceProvider()
797+
- pattern: new RC2CryptoServiceProvider()
798+
- pattern: TripleDES.Create()
799+
- pattern: DES.Create()
800+
- pattern: RC2.Create()
801+
# Obsolete RijndaelManaged (use Aes.Create() instead)
802+
- pattern: new RijndaelManaged()
803+
# Using raw password bytes directly as crypto key (no KDF)
634804
- pattern: new RijndaelManaged() { Key = Encoding.UTF8.GetBytes($KEY) }
805+
- pattern: new AesCryptoServiceProvider() { Key = Encoding.UTF8.GetBytes($KEY) }
635806
metadata:
636807
category: security
637808
owasp: A02
638809
cwe: CWE-327
639-
confidence: medium
810+
confidence: high
640811
subcategory: crypto
641812
vulnerability_class: "Cryptographic Weakness"
642-
fix: "Use SHA256.Create() instead of MD5/SHA1. Use Aes.Create() with CipherMode.CBC or AesGcm for encryption."
813+
fix: "Use Aes.Create() instead of 3DES/DES/RC2. Use Rfc2898DeriveBytes or HKDF for key derivation from passwords. Never use raw Encoding.GetBytes() as a crypto key."
643814

644815
# A03: Injection (additional patterns)
645816
- id: dotnet-xpath-injection

0 commit comments

Comments
 (0)