@brunosps00/dev-workflow 0.5.0 → 0.6.1
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/README.md +7 -3
- package/lib/constants.js +2 -0
- package/lib/install-deps.js +39 -1
- package/package.json +1 -1
- package/scaffold/en/commands/dw-brainstorm.md +50 -15
- package/scaffold/en/commands/dw-code-review.md +10 -0
- package/scaffold/en/commands/dw-create-prd.md +10 -1
- package/scaffold/en/commands/dw-generate-pr.md +4 -1
- package/scaffold/en/commands/dw-help.md +3 -0
- package/scaffold/en/commands/dw-quick.md +9 -7
- package/scaffold/en/commands/dw-review-implementation.md +11 -0
- package/scaffold/en/commands/dw-security-check.md +271 -0
- package/scaffold/en/templates/brainstorm-matrix.md +12 -4
- package/scaffold/en/templates/idea-onepager.md +90 -0
- package/scaffold/pt-br/commands/dw-brainstorm.md +54 -19
- package/scaffold/pt-br/commands/dw-code-review.md +10 -0
- package/scaffold/pt-br/commands/dw-create-prd.md +10 -1
- package/scaffold/pt-br/commands/dw-generate-pr.md +4 -1
- package/scaffold/pt-br/commands/dw-help.md +3 -0
- package/scaffold/pt-br/commands/dw-quick.md +9 -7
- package/scaffold/pt-br/commands/dw-review-implementation.md +11 -0
- package/scaffold/pt-br/commands/dw-security-check.md +271 -0
- package/scaffold/pt-br/templates/brainstorm-matrix.md +12 -4
- package/scaffold/pt-br/templates/idea-onepager.md +90 -0
- package/scaffold/skills/security-review/languages/csharp.md +507 -0
- package/scaffold/skills/security-review/languages/rust.md +584 -0
- package/scaffold/skills/security-review/languages/typescript.md +373 -0
|
@@ -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`
|