@brunosps00/dev-workflow 0.5.0 → 0.6.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.
@@ -0,0 +1,507 @@
1
+ # C# / .NET Security Patterns
2
+
3
+ Covers **ASP.NET Core, ASP.NET (Framework), Blazor, Razor Pages, Minimal APIs, Entity Framework Core, ADO.NET, Dapper, Identity, NuGet**. Used by `/dw-security-check` as the primary reference when C# is detected in scope.
4
+
5
+ ---
6
+
7
+ ## Framework Detection
8
+
9
+ | Indicator | Framework |
10
+ |-----------|-----------|
11
+ | `Microsoft.AspNetCore.*`, `Program.cs` with `WebApplication.CreateBuilder` | ASP.NET Core (6+) |
12
+ | `Startup.cs` with `ConfigureServices` + `Configure` | ASP.NET Core (legacy) |
13
+ | `System.Web.Mvc`, `Global.asax` | ASP.NET MVC (Framework) |
14
+ | `@page` at top of `.razor` / `.cshtml` | Razor Pages / Blazor |
15
+ | `Microsoft.EntityFrameworkCore.*` | EF Core |
16
+ | `System.Data.SqlClient`, `Microsoft.Data.SqlClient` | ADO.NET |
17
+ | `Dapper` namespace | Dapper |
18
+ | `Microsoft.AspNetCore.Identity` | ASP.NET Identity |
19
+
20
+ ---
21
+
22
+ ## SQL Injection
23
+
24
+ ### Flag These
25
+
26
+ ```csharp
27
+ // FLAG CRITICAL: EF Core FromSqlRaw with string interpolation
28
+ var users = ctx.Users.FromSqlRaw($"SELECT * FROM Users WHERE Name = '{userInput}'");
29
+
30
+ // FLAG CRITICAL: ADO.NET CommandText concatenation
31
+ var cmd = new SqlCommand("SELECT * FROM Users WHERE Name = '" + userInput + "'", conn);
32
+
33
+ // FLAG CRITICAL: Dapper with string interpolation
34
+ var users = conn.Query<User>($"SELECT * FROM Users WHERE Id = {userId}");
35
+
36
+ // FLAG HIGH: Dynamic LINQ with string expressions from input
37
+ var result = db.Users.Where(dynamicExpr); // System.Linq.Dynamic.Core with user string
38
+ ```
39
+
40
+ ### Safe Patterns
41
+
42
+ ```csharp
43
+ // SAFE: EF Core FromSqlInterpolated (binds parameters)
44
+ var users = ctx.Users.FromSqlInterpolated($"SELECT * FROM Users WHERE Name = {userInput}");
45
+
46
+ // SAFE: EF Core LINQ (always parameterized)
47
+ var users = ctx.Users.Where(u => u.Name == userInput).ToList();
48
+
49
+ // SAFE: ADO.NET parameterized
50
+ using var cmd = new SqlCommand("SELECT * FROM Users WHERE Name = @name", conn);
51
+ cmd.Parameters.Add("@name", SqlDbType.NVarChar).Value = userInput;
52
+
53
+ // SAFE: Dapper parameterized
54
+ var users = conn.Query<User>("SELECT * FROM Users WHERE Id = @Id", new { Id = userId });
55
+ ```
56
+
57
+ ---
58
+
59
+ ## XSS
60
+
61
+ ### Flag These (Razor / Blazor)
62
+
63
+ ```csharp
64
+ // FLAG CRITICAL: @Html.Raw with user input
65
+ @Html.Raw(Model.Description)
66
+
67
+ // FLAG CRITICAL: MvcHtmlString from user input
68
+ new MvcHtmlString(userInput)
69
+
70
+ // FLAG CRITICAL: Blazor MarkupString from user input
71
+ <div>@((MarkupString)userInput)</div>
72
+
73
+ // FLAG CRITICAL: Response.Write without encoding (legacy)
74
+ Response.Write(userInput);
75
+
76
+ // FLAG HIGH: @Html.DisplayFor on already-HTML content without explicit encoding
77
+ // Usually safe, but check custom templates that call Html.Raw internally
78
+
79
+ // FLAG HIGH: IHtmlContent built from user input without HtmlEncoder
80
+ new HtmlString(userInput)
81
+ ```
82
+
83
+ ### Safe Patterns
84
+
85
+ ```csharp
86
+ // SAFE: Razor auto-encodes @Expression
87
+ <div>@Model.Description</div> // encoded
88
+
89
+ // SAFE: explicit encoding when needed
90
+ @Html.Encode(userInput)
91
+ @System.Net.WebUtility.HtmlEncode(userInput)
92
+
93
+ // SAFE: Blazor @text interpolation encodes by default
94
+ <div>@userInput</div>
95
+ ```
96
+
97
+ ### Anti-Forgery (CSRF)
98
+
99
+ ```csharp
100
+ // FLAG CRITICAL: Controller action modifies state without [ValidateAntiForgeryToken]
101
+ [HttpPost]
102
+ public IActionResult UpdateProfile(ProfileDto dto) { /* ... */ }
103
+
104
+ // FLAG HIGH: [IgnoreAntiforgeryToken] on sensitive endpoint
105
+ [IgnoreAntiforgeryToken] [HttpPost] public IActionResult DeleteAccount() { /* ... */ }
106
+
107
+ // FLAG HIGH: Global AntiForgery disabled
108
+ services.AddControllers().ConfigureApiBehaviorOptions(o => { /* ... */ });
109
+ // plus no [AutoValidateAntiforgeryToken] filter
110
+
111
+ // SAFE
112
+ [HttpPost, ValidateAntiForgeryToken]
113
+ public IActionResult UpdateProfile(ProfileDto dto) { /* ... */ }
114
+
115
+ // Or globally (Razor Pages + MVC)
116
+ services.AddMvc(o => o.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Deserialization
122
+
123
+ ### Always Flag
124
+
125
+ ```csharp
126
+ // FLAG CRITICAL: BinaryFormatter — deprecated, known RCE gadget chains
127
+ var bf = new BinaryFormatter();
128
+ var obj = bf.Deserialize(stream); // arbitrary type instantiation
129
+
130
+ // FLAG CRITICAL: NetDataContractSerializer — same class of vulnerability
131
+ var ndcs = new NetDataContractSerializer();
132
+ ndcs.ReadObject(stream);
133
+
134
+ // FLAG CRITICAL: LosFormatter (ViewState) with untrusted input
135
+ var los = new LosFormatter();
136
+ los.Deserialize(userSerialized);
137
+
138
+ // FLAG CRITICAL: JavaScriptSerializer with type info (pre-netcore)
139
+ new JavaScriptSerializer(new SimpleTypeResolver()).Deserialize<object>(userInput);
140
+
141
+ // FLAG CRITICAL: Newtonsoft.Json with TypeNameHandling != None
142
+ var obj = JsonConvert.DeserializeObject<T>(userJson, new JsonSerializerSettings {
143
+ TypeNameHandling = TypeNameHandling.All // or .Objects / .Auto
144
+ });
145
+
146
+ // FLAG HIGH: System.Text.Json with TypeInfoResolver accepting arbitrary $type
147
+
148
+ // FLAG HIGH: XmlSerializer with polymorphic types from user XML
149
+ ```
150
+
151
+ ### Safe Patterns
152
+
153
+ ```csharp
154
+ // SAFE: System.Text.Json with closed-type deserialization
155
+ var obj = JsonSerializer.Deserialize<UserDto>(userJson); // concrete type only
156
+
157
+ // SAFE: Newtonsoft with TypeNameHandling.None (default)
158
+ var obj = JsonConvert.DeserializeObject<UserDto>(userJson);
159
+
160
+ // SAFE: DataContractSerializer with KnownTypes allowlist
161
+ [DataContract]
162
+ [KnownType(typeof(UserDto))]
163
+ public abstract class EntityDto { }
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Authentication & Authorization
169
+
170
+ ### Flag These
171
+
172
+ ```csharp
173
+ // FLAG CRITICAL: [AllowAnonymous] on sensitive controller
174
+ [AllowAnonymous] [HttpPost("admin/reset-all-passwords")]
175
+ public IActionResult ResetAll() { /* ... */ }
176
+
177
+ // FLAG CRITICAL: missing [Authorize] on controller and global auth not configured
178
+ public class AdminController : Controller { public IActionResult Delete(int id) { } }
179
+
180
+ // FLAG HIGH: Cookie auth without secure flags
181
+ services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
182
+ .AddCookie(options => {
183
+ options.Cookie.SecurePolicy = CookieSecurePolicy.None; // FLAG
184
+ options.Cookie.HttpOnly = false; // FLAG
185
+ options.Cookie.SameSite = SameSiteMode.None; // FLAG without Secure=true
186
+ });
187
+
188
+ // FLAG HIGH: JWT validation without issuer / audience / lifetime
189
+ new TokenValidationParameters {
190
+ ValidateIssuer = false, // FLAG
191
+ ValidateAudience = false, // FLAG
192
+ ValidateLifetime = false, // FLAG CRITICAL
193
+ ValidateIssuerSigningKey = false, // FLAG CRITICAL
194
+ RequireExpirationTime = false, // FLAG
195
+ }
196
+
197
+ // FLAG HIGH: Password hashing with weak algorithm
198
+ var hash = BitConverter.ToString(MD5.HashData(Encoding.UTF8.GetBytes(pass)));
199
+
200
+ // FLAG HIGH: Identity with weak password policy
201
+ services.Configure<IdentityOptions>(o => {
202
+ o.Password.RequiredLength = 4; // FLAG
203
+ o.Password.RequireDigit = false;
204
+ o.Password.RequireNonAlphanumeric = false;
205
+ o.Lockout.MaxFailedAccessAttempts = 1000; // FLAG
206
+ });
207
+
208
+ // FLAG HIGH: IDOR — no ownership check
209
+ public IActionResult GetInvoice(int id) {
210
+ return Ok(db.Invoices.Find(id)); // no userId == invoice.OwnerId check
211
+ }
212
+ ```
213
+
214
+ ### Safe Baselines
215
+
216
+ ```csharp
217
+ // Global auth requirement (fallback)
218
+ services.AddAuthorization(o => {
219
+ o.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
220
+ });
221
+
222
+ // Cookie hardening
223
+ options.Cookie.HttpOnly = true;
224
+ options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
225
+ options.Cookie.SameSite = SameSiteMode.Strict;
226
+ options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
227
+ options.SlidingExpiration = true;
228
+
229
+ // Password hashing — use ASP.NET Identity's PasswordHasher (PBKDF2) or BCrypt.Net-Next
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Secrets
235
+
236
+ ### Flag These
237
+
238
+ ```csharp
239
+ // FLAG CRITICAL: hardcoded connection string with credentials
240
+ services.AddDbContext<AppDb>(o => o.UseSqlServer("Server=x;User=sa;Password=P@ss1;"));
241
+
242
+ // FLAG CRITICAL: appsettings.json in git with Production secrets
243
+ // (scan for: "ConnectionStrings", "ApiKey", "Secret", "Token" in appsettings*.json)
244
+
245
+ // FLAG HIGH: secret in logs
246
+ logger.LogInformation("Connecting with {cs}", builder.ConnectionString);
247
+
248
+ // FLAG HIGH: Environment variable accessed without guard
249
+ var key = Environment.GetEnvironmentVariable("STRIPE_SECRET"); // returns null silently if missing
250
+ ```
251
+
252
+ ### Safe Patterns
253
+
254
+ ```csharp
255
+ // User Secrets (dev only)
256
+ // dotnet user-secrets set "Stripe:SecretKey" "sk_test_..."
257
+
258
+ // Azure Key Vault / AWS Secrets Manager
259
+ builder.Configuration.AddAzureKeyVault(new Uri(vaultUri), new DefaultAzureCredential());
260
+
261
+ // Options pattern with validation
262
+ services.AddOptions<StripeOptions>()
263
+ .Bind(builder.Configuration.GetSection("Stripe"))
264
+ .ValidateDataAnnotations()
265
+ .ValidateOnStart();
266
+ ```
267
+
268
+ ---
269
+
270
+ ## SSRF
271
+
272
+ ```csharp
273
+ // FLAG CRITICAL: HttpClient with attacker-controlled URL
274
+ var resp = await http.GetStringAsync(userUrl);
275
+
276
+ // FLAG CRITICAL: WebRequest.Create with user input
277
+ var req = WebRequest.Create(userUrl); // deprecated + SSRF
278
+
279
+ // FLAG HIGH: Redirect on user-controlled URL (open redirect)
280
+ return Redirect(userUrl); // Unvalidated redirect
281
+
282
+ // SAFE: allowlist-based fetch
283
+ var host = new Uri(userUrl).Host;
284
+ if (!allowedHosts.Contains(host)) throw new SecurityException("host not allowed");
285
+ var resp = await http.GetStringAsync(userUrl);
286
+
287
+ // SAFE: LocalRedirect prevents external redirection
288
+ return LocalRedirect(userUrl); // throws if absolute URL
289
+ ```
290
+
291
+ ---
292
+
293
+ ## Path Traversal
294
+
295
+ ```csharp
296
+ // FLAG CRITICAL: direct user path
297
+ var content = File.ReadAllText(userPath);
298
+
299
+ // FLAG CRITICAL: Path.Combine without containment
300
+ var full = Path.Combine(baseDir, userFile); // ../../../etc/passwd still works
301
+
302
+ // FLAG HIGH: Server.MapPath with user input (ASP.NET Framework)
303
+ var full = Server.MapPath(userRelative);
304
+
305
+ // SAFE: canonicalize + contained check
306
+ var canonical = Path.GetFullPath(Path.Combine(baseDir, userFile));
307
+ if (!canonical.StartsWith(Path.GetFullPath(baseDir) + Path.DirectorySeparatorChar))
308
+ throw new UnauthorizedAccessException("path traversal");
309
+ ```
310
+
311
+ ---
312
+
313
+ ## Cryptography
314
+
315
+ ### Flag These
316
+
317
+ ```csharp
318
+ // FLAG CRITICAL: MD5 / SHA1 for passwords or security tokens
319
+ using var md5 = MD5.Create();
320
+ var hash = md5.ComputeHash(bytes);
321
+
322
+ // FLAG CRITICAL: Random for security tokens / session ids
323
+ var id = new Random().Next();
324
+
325
+ // FLAG HIGH: low PBKDF2 iterations
326
+ var kdf = new Rfc2898DeriveBytes(password, salt, iterations: 1000); // too low (2026 baseline ≥ 600k for SHA-256)
327
+
328
+ // FLAG HIGH: DES / 3DES / ECB mode
329
+ var aes = Aes.Create();
330
+ aes.Mode = CipherMode.ECB; // FLAG
331
+
332
+ // FLAG HIGH: hardcoded IV or predictable IV
333
+ var iv = new byte[16]; // zeros
334
+ ```
335
+
336
+ ### Safe Patterns
337
+
338
+ ```csharp
339
+ // SAFE: cryptographic RNG
340
+ var bytes = new byte[32];
341
+ RandomNumberGenerator.Fill(bytes);
342
+ var token = Convert.ToHexString(bytes);
343
+
344
+ // SAFE: modern PBKDF2
345
+ var kdf = new Rfc2898DeriveBytes(password, salt, 600_000, HashAlgorithmName.SHA256);
346
+
347
+ // Or BCrypt.Net-Next / Argon2 (via Konscious.Security.Cryptography)
348
+ var hash = BCrypt.Net.BCrypt.HashPassword(password, workFactor: 12);
349
+
350
+ // AES with CBC/GCM + random IV per message
351
+ aes.Mode = CipherMode.CBC;
352
+ aes.GenerateIV();
353
+ ```
354
+
355
+ ---
356
+
357
+ ## Supply Chain (NuGet)
358
+
359
+ ### Flag These
360
+
361
+ - `PackageReference` with floating versions on security-sensitive deps: `<PackageReference Include="Newtonsoft.Json" Version="*" />`
362
+ - Missing `packages.lock.json` in CI with `--locked-mode`
363
+ - Private feeds configured without authentication — confusion attack surface
364
+ - Old `Newtonsoft.Json` versions (< 13.0.1) — known CVE classes
365
+ - Abandoned / archived packages still in use
366
+
367
+ ### Safe Patterns
368
+
369
+ ```xml
370
+ <!-- Directory.Build.props -->
371
+ <PropertyGroup>
372
+ <RestoreLockedMode>true</RestoreLockedMode>
373
+ <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
374
+ </PropertyGroup>
375
+
376
+ <!-- .csproj -->
377
+ <ItemGroup>
378
+ <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.12" />
379
+ </ItemGroup>
380
+ ```
381
+
382
+ Run `dotnet list package --vulnerable --include-transitive` as part of CI.
383
+
384
+ ---
385
+
386
+ ## ASP.NET Core Middleware Order
387
+
388
+ Middleware order matters for security. Flag if order is wrong:
389
+
390
+ ```csharp
391
+ var app = builder.Build();
392
+
393
+ // SAFE BASELINE ORDER
394
+ app.UseHttpsRedirection();
395
+ app.UseHsts(); // only in Production
396
+ app.UseStaticFiles();
397
+ app.UseRouting();
398
+ app.UseCors(policy); // after UseRouting, before Authentication
399
+ app.UseAuthentication();
400
+ app.UseAuthorization();
401
+ app.UseAntiforgery(); // MVC/Razor Pages
402
+ app.MapControllers();
403
+
404
+ // FLAG HIGH: Authorization before Authentication
405
+ app.UseAuthorization();
406
+ app.UseAuthentication(); // too late
407
+
408
+ // FLAG HIGH: UseCors(allow-anything) in Production
409
+ app.UseCors(p => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
410
+
411
+ // FLAG HIGH: missing UseHsts + UseHttpsRedirection in Production
412
+ ```
413
+
414
+ ---
415
+
416
+ ## Minimal APIs
417
+
418
+ ```csharp
419
+ // FLAG HIGH: endpoint without [Authorize] or .RequireAuthorization()
420
+ app.MapPost("/api/admin/reset", async (Db db) => { /* ... */ });
421
+
422
+ // FLAG HIGH: model binding without validation
423
+ app.MapPost("/users", (UserDto dto, Db db) => db.Users.Add(dto));
424
+ // Without [AsParameters] + DataAnnotations or FluentValidation
425
+
426
+ // SAFE
427
+ app.MapPost("/api/admin/reset", Reset).RequireAuthorization("Admin");
428
+ ```
429
+
430
+ ---
431
+
432
+ ## .NET Remoting / WCF (Legacy)
433
+
434
+ Flag on sight in modern codebases:
435
+
436
+ - `System.Runtime.Remoting.*` usage
437
+ - WCF `NetTcpBinding` with `TransferMode.Streamed` + `BinaryFormatter` serialization
438
+ - `NetDataContractSerializer` anywhere
439
+
440
+ These have a history of deserialization RCE and are not recommended for new code. Migrate to gRPC / HTTP APIs.
441
+
442
+ ---
443
+
444
+ ## Research Checklist (before flagging)
445
+
446
+ 1. **Is the input attacker-controlled?** — trace from `HttpContext.Request`, `IFormFile`, `[FromBody]`, `[FromQuery]`, external API. Values from `IConfiguration`/env are not attacker-controlled.
447
+ 2. **Does a validator run upstream?** — check DataAnnotations, FluentValidation, custom `IActionFilter`, Minimal API `IEndpointFilter`.
448
+ 3. **Is the ORM call parameterized?** — EF Core LINQ and `FromSqlInterpolated` are safe; `FromSqlRaw` with concatenation is not.
449
+ 4. **Is the auth requirement set globally?** — look for `FallbackPolicy` or `AutoValidateAntiforgeryTokenAttribute` before claiming missing `[Authorize]`.
450
+ 5. **Is the deserializer type-closed?** — `System.Text.Json.Deserialize<ConcreteType>` without `$type` is safe.
451
+ 6. **Is the path contained?** — `Path.GetFullPath` + StartsWith-check is the correct pattern.
452
+
453
+ Only report findings that pass "attacker-controlled input + missing mitigation + exploitable sink".
454
+
455
+ ---
456
+
457
+ ## Grep Patterns
458
+
459
+ ```bash
460
+ # SQL injection
461
+ grep -rn "FromSqlRaw\|ExecuteSqlRaw" --include="*.cs"
462
+ grep -rn "new SqlCommand(.*\+" --include="*.cs"
463
+ grep -rn 'conn\.Query.*\${' --include="*.cs"
464
+
465
+ # XSS
466
+ grep -rn "Html\.Raw\|MvcHtmlString\|new HtmlString\|(MarkupString)" --include="*.cs" --include="*.cshtml" --include="*.razor"
467
+
468
+ # Anti-forgery disabled
469
+ grep -rn "IgnoreAntiforgeryToken\|DisableFormValueModelBinding" --include="*.cs"
470
+
471
+ # Dangerous deserialization
472
+ grep -rn "BinaryFormatter\|NetDataContractSerializer\|LosFormatter\|JavaScriptSerializer" --include="*.cs"
473
+ grep -rn "TypeNameHandling\s*=\s*TypeNameHandling\.\(All\|Objects\|Auto\)" --include="*.cs"
474
+
475
+ # Auth anti-patterns
476
+ grep -rn "AllowAnonymous" --include="*.cs"
477
+ grep -rn "ValidateLifetime\s*=\s*false\|ValidateIssuerSigningKey\s*=\s*false" --include="*.cs"
478
+
479
+ # Crypto
480
+ grep -rn "MD5\.Create\|SHA1\.Create\|new Random(" --include="*.cs"
481
+ grep -rn "CipherMode\.ECB" --include="*.cs"
482
+ grep -rn "Rfc2898DeriveBytes.*," --include="*.cs" # then inspect iterations
483
+
484
+ # Path traversal
485
+ grep -rn "File\.Read.*Combine\|Server\.MapPath" --include="*.cs"
486
+
487
+ # Secrets in config
488
+ grep -rEn '"(ApiKey|Secret|Password|ConnectionStrings)":\s*"[^"]+' appsettings*.json
489
+
490
+ # Hardcoded connection strings
491
+ grep -rn "UseSqlServer(\"Server=" --include="*.cs"
492
+ ```
493
+
494
+ ---
495
+
496
+ ## Cross-Reference
497
+
498
+ For general concepts not specific to C#:
499
+ - `../references/xss.md`
500
+ - `../references/injection.md`
501
+ - `../references/deserialization.md`
502
+ - `../references/csrf.md`
503
+ - `../references/authentication.md`
504
+ - `../references/authorization.md`
505
+ - `../references/cryptography.md`
506
+ - `../references/supply-chain.md`
507
+ - `../references/ssrf.md`