Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
Application Security

ASP.NET Core SAST Security Checklist 2026

ASP.NET Core SAST checklist: 60+ checks for SQL injection, XSS, CORS, weak crypto, secrets and EF Core misuse — with C# code examples and tool guidance.

Offensive360 Security Research Team — min read
ASP.NET Core security C# security .NET security SAST static code analysis C# .NET static code analysis C# static code analysis tools application security checklist ASP.NET Core SAST dotnet security checklist C# SAST Entity Framework security Roslyn analyzers application security audit checklist

This checklist covers every significant vulnerability class that static application security testing (SAST) should detect in ASP.NET Core and C# codebases — with code examples showing the vulnerable pattern, the secure fix, and the Roslyn rule or SAST finding that catches it.

Use this as a review guide before deploying a new ASP.NET Core application, as a SAST tool evaluation benchmark, or as a structured reference for C# security code reviews.


How to Use This Checklist

Each section maps to a vulnerability class detectable through static code analysis. For each item:

  • Roslyn rule — the CA rule that catches it in dotnet build (single-method patterns only)
  • SAST required — whether a dedicated SAST tool with interprocedural taint analysis is needed for reliable detection
  • Code examples — the vulnerable pattern and the secure replacement

Quick note on Roslyn vs. SAST tools: Roslyn analyzers (built into the .NET SDK) catch obvious single-method patterns and are free. They cannot trace data flows across method calls, class boundaries, or from/to the database. For the majority of injection vulnerabilities in real ASP.NET Core applications — where input travels from a controller through a service layer and into a repository — a dedicated .NET SAST tool with interprocedural taint analysis is required.


1. SQL Injection

1.1 ADO.NET String Concatenation (CWE-89)

Roslyn: CA2100 | SAST: Required for cross-method chains

// ❌ VULNERABLE — user input in SqlCommand text
string username = Request.QueryString["username"];
string sql = "SELECT * FROM Users WHERE Username = '" + username + "'";
var cmd = new SqlCommand(sql, connection);

// ✅ SECURE — parameterized query
var cmd = new SqlCommand("SELECT * FROM Users WHERE Username = @u", connection);
cmd.Parameters.AddWithValue("@u", username);

Checklist:

  • No SqlCommand, OleDbCommand, or OdbcCommand initialized with string concatenation or interpolation
  • All CommandText values are either compile-time constants or parameterized with @param placeholders
  • Stored procedure calls use CommandType.StoredProcedure and named parameters

1.2 Entity Framework FromSqlRaw Injection (CWE-89)

Roslyn: Not detected | SAST: Required

// ❌ VULNERABLE — string interpolation bypasses EF parameterization
var id = Request.Form["id"];
var user = dbContext.Users
    .FromSqlRaw($"SELECT * FROM Users WHERE Id = {id}")
    .FirstOrDefault();

// ✅ SECURE — use LINQ (auto-parameterized)
var user = dbContext.Users.FirstOrDefault(u => u.Id == int.Parse(id));

// ✅ ALSO SECURE — FromSqlInterpolated safely parameterizes
var user = dbContext.Users
    .FromSqlInterpolated($"SELECT * FROM Users WHERE Id = {id}")
    .FirstOrDefault();

Checklist:

  • No FromSqlRaw() calls with string interpolation ($"...") or concatenation (+)
  • No ExecuteSqlRaw() calls with dynamic input
  • FromSqlInterpolated() or LINQ queries used for all dynamic data access
  • Dynamic ORDER BY columns validated against a strict allowlist before embedding in SQL

1.3 Second-Order SQL Injection (CWE-89)

Roslyn: Not detected | SAST: Required (interprocedural + database taint tracking)

// Stage 1: Safe storage (correctly parameterized)
dbContext.Users.Add(new User { Username = username }); // "admin'--" stored safely

// Stage 2: ❌ VULNERABLE — retrieves from DB and uses unsafely
string storedUsername = await GetUsernameFromDb(userId); // Returns "admin'--"
string sql = $"SELECT * FROM Logs WHERE Username = '{storedUsername}'";

// ✅ SECURE — parameterize even database-sourced values
var logs = await dbContext.Logs
    .FromSqlInterpolated($"SELECT * FROM Logs WHERE Username = {storedUsername}")
    .ToListAsync();

Checklist:

  • Database-sourced values treated as untrusted and parameterized in subsequent queries
  • No assumption that “it came from our database, so it must be safe”
  • SAST tool configured to track taint through database read/write operations

2. Cross-Site Scripting (XSS)

2.1 Razor View XSS via @Html.Raw (CWE-79)

Roslyn: Not detected | SAST: Required

@* ❌ VULNERABLE — bypasses Razor's automatic HTML encoding *@
@Html.Raw(Model.UserBio)

@* ❌ ALSO VULNERABLE — JavaScript context without encoding *@
<script>var userName = '@Model.Username';</script>

@* ✅ SECURE — Razor encodes by default *@
@Model.UserBio

@* ✅ SECURE — JavaScript string encoding for JS context *@
<script>var userName = '@Html.JavaScriptStringEncode(Model.Username)';</script>

Checklist:

  • No @Html.Raw() with user-controlled content
  • No @MvcHtmlString.Create() with unencoded input
  • JavaScript contexts in Razor templates use @Html.JavaScriptStringEncode()
  • Content-Security-Policy header configured to restrict inline scripts

2.2 HttpResponse.Write XSS (CWE-79)

Roslyn: CA3001 | SAST: Required for multi-method chains

// ❌ VULNERABLE — user input written directly to response
string comment = Request.Form["comment"];
Response.Write("<p>" + comment + "</p>");

// ✅ SECURE — HTML encode before output
Response.Write("<p>" + HttpUtility.HtmlEncode(comment) + "</p>");

Checklist:

  • No Response.Write() or HttpContext.Response.WriteAsync() with unencoded user input
  • All string interpolation into HTML output uses HttpUtility.HtmlEncode() or equivalent

3. Command Injection

3.1 Process.Start with User Input (CWE-78)

Roslyn: CA3006 | SAST: Required for cross-method chains

// ❌ VULNERABLE — user input in shell command
string filename = Request.QueryString["file"];
Process.Start("cmd.exe", "/c convert " + filename + " output.pdf");

// ✅ SECURE — validate input + use ArgumentList (no shell parsing)
if (!Regex.IsMatch(filename, @"^[a-zA-Z0-9_\-]+\.pdf$"))
    return BadRequest("Invalid filename");

var psi = new ProcessStartInfo
{
    FileName = "convert",
    UseShellExecute = false
};
psi.ArgumentList.Add(filename);
psi.ArgumentList.Add("output.pdf");
Process.Start(psi);

Checklist:

  • No Process.Start() with Arguments built by string concatenation or interpolation
  • When OS commands are required, ArgumentList used instead of the string Arguments property
  • UseShellExecute = false to disable shell interpretation of arguments
  • Input validated against a strict allowlist before use in any OS operation

4. Path Traversal

4.1 File Path with User-Controlled Input (CWE-22)

Roslyn: CA3003 | SAST: Required for cross-method chains

// ❌ VULNERABLE — user controls part of the file path
string filename = Request.QueryString["file"];
string path = Path.Combine("/uploads/", filename);
return File(path, "application/octet-stream");
// ?file=../../etc/passwd → reads system files

// ✅ SECURE — normalize and validate the resolved path
string uploadDir = Path.GetFullPath("/uploads/");
string filePath = Path.GetFullPath(Path.Combine(uploadDir, filename));

if (!filePath.StartsWith(uploadDir, StringComparison.OrdinalIgnoreCase))
    return BadRequest("Invalid file path");

return File(filePath, "application/octet-stream");

Checklist:

  • All file paths normalized with Path.GetFullPath() before comparison
  • Resolved path verified to start with the intended base directory
  • No user-supplied filenames used directly in File(), System.IO.File.ReadAllBytes(), or similar

5. Server-Side Request Forgery (SSRF)

5.1 HttpClient with User-Controlled URL (CWE-918)

Roslyn: Not detected | SAST: Required

// ❌ VULNERABLE — user controls the target URL
string url = Request.Query["url"];
var response = await httpClient.GetAsync(url);
// Attacker: ?url=http://169.254.169.254/latest/meta-data/ → AWS metadata

// ✅ SECURE — validate URL against an allowlist
var uri = new Uri(url);
string[] allowedHosts = { "api.partner.com", "cdn.yoursite.com" };

if (!allowedHosts.Contains(uri.Host, StringComparer.OrdinalIgnoreCase))
    return BadRequest("URL not permitted");

var response = await httpClient.GetAsync(uri);

Checklist:

  • No HttpClient.GetAsync(), PostAsync(), or similar with user-controlled URLs
  • URL allowlist validated against the parsed Uri.Host (not string matching against the raw URL)
  • Redirect following disabled or validated (AllowAutoRedirect = false)

6. Insecure Deserialization

6.1 BinaryFormatter Usage (CWE-502)

Roslyn: CA5360 | SAST: Can detect with type tracking

// ❌ VULNERABLE — arbitrary code execution; deprecated in .NET 5+
BinaryFormatter formatter = new BinaryFormatter();
object obj = formatter.Deserialize(requestStream);

// ✅ SECURE — use System.Text.Json with a specific type
var options = new JsonSerializerOptions { MaxDepth = 32 };
var obj = JsonSerializer.Deserialize<MySpecificType>(json, options);

Checklist:

  • No BinaryFormatter, SoapFormatter, or ObjectStateFormatter usage
  • No JavaScriptSerializer.Deserialize<object>() or deserialization to dynamic/object
  • Newtonsoft.Json TypeNameHandling not set to All or Auto with untrusted input
  • Deserialization targets a specific, known type — not a base class or object

7. Weak Cryptography

7.1 Weak Hash Algorithms (CWE-327 / CWE-328)

Roslyn: CA5351 (MD5, DES) | SAST: Required for indirect usage

// ❌ VULNERABLE — MD5 for password hashing
using var md5 = MD5.Create();
byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(password));

// ✅ SECURE — PBKDF2 with SHA-512 (or use ASP.NET Identity's PasswordHasher<T>)
using var kdf = new Rfc2898DeriveBytes(
    password,
    salt: RandomNumberGenerator.GetBytes(16),
    iterations: 600_000,
    HashAlgorithmName.SHA512
);
byte[] hash = kdf.GetBytes(32);

7.2 Weak Symmetric Encryption (CWE-327)

// ❌ VULNERABLE — DES has a 56-bit effective key (trivially brute-forced)
using var des = DES.Create();

// ❌ ALSO WEAK — 3DES (112-bit effective key; deprecated by NIST 2023)
using var tdes = TripleDES.Create();

// ✅ SECURE — AES-256-GCM (authenticated encryption)
using var aes = new AesGcm(key, tagSizeInBytes: 16);
aes.Encrypt(nonce, plaintext, ciphertext, tag);

Checklist:

  • No MD5.Create(), SHA1.Create(), DES.Create(), RC2.Create(), or TripleDES.Create() for security-sensitive operations
  • Password hashing uses PasswordHasher<T> (ASP.NET Identity), Rfc2898DeriveBytes, or Argon2
  • Symmetric encryption uses AesGcm (authenticated) or AesCbc with a separate HMAC
  • RNGCryptoServiceProvider or RandomNumberGenerator.GetBytes() for all security tokens (not System.Random)

8. Hardcoded Secrets

8.1 Connection Strings and API Keys in Source (CWE-798)

Roslyn: Not detected (pattern-only tools catch obvious cases) | SAST: Required

// ❌ VULNERABLE — credentials in source code
private const string ConnectionString =
    "Server=prod-db.company.com;Database=App;User=sa;Password=Prod@2024!";

private readonly string _apiKey = "sk-live-abc123def456...";

// ✅ SECURE — credentials from configuration or Key Vault
private readonly string _connectionString;
private readonly string _apiKey;

public MyService(IConfiguration config)
{
    _connectionString = config.GetConnectionString("DefaultConnection");
    _apiKey = config["ExternalService:ApiKey"];
    // Values loaded at runtime from env vars, user secrets, or Azure Key Vault
}

Checklist:

  • No connection strings containing Password= or pwd= in source code
  • No hardcoded API keys, tokens, or OAuth client secrets as string literals
  • appsettings.json contains no production secrets — only non-sensitive configuration
  • Git history scanned with gitleaks or truffleHog for previously committed secrets
  • Secrets managed via Azure Key Vault, AWS Secrets Manager, HashiCorp Vault, or equivalent

9. Mass Assignment / Over-Posting

9.1 EF Entity Used Directly as API Model (OWASP A01)

Roslyn: Not detected | SAST: Required (model binding taint tracking)

// ❌ VULNERABLE — binds all entity properties from the request body
[HttpPost]
public async Task<IActionResult> UpdateProfile([FromBody] User user)
{
    dbContext.Users.Update(user); // user.IsAdmin, user.Role can be set by attacker
    await dbContext.SaveChangesAsync();
    return Ok();
}

// ✅ SECURE — DTO with only user-editable fields
public record UpdateProfileDto(string DisplayName, string Email, string? Bio);

[HttpPost]
public async Task<IActionResult> UpdateProfile([FromBody] UpdateProfileDto dto)
{
    var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
    var entity = await dbContext.Users.FindAsync(userId);
    if (entity is null) return NotFound();

    entity.DisplayName = dto.DisplayName;
    entity.Email = dto.Email;
    entity.Bio = dto.Bio;
    // entity.IsAdmin and entity.Role are never touched

    await dbContext.SaveChangesAsync();
    return Ok();
}

Checklist:

  • EF Core entities never used directly as [FromBody] or [FromQuery] model-bound parameters
  • DTOs with explicit field lists used for all API input
  • [BindNever] attribute applied to sensitive entity properties where entities must be used

10. CORS Misconfiguration

10.1 Wildcard CORS on Authenticated Endpoints (CWE-942)

Roslyn: Not detected | SAST: Required (CORS middleware configuration analysis)

// ❌ VULNERABLE — wildcard CORS on authenticated API
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
        policy.AllowAnyOrigin()  // * — allows any website to call this API
              .AllowAnyMethod()
              .AllowAnyHeader()
              .AllowCredentials()); // BLOCKED by browsers, but dangerous if reflected
});

// ✅ SECURE — explicit origin allowlist
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
        policy.WithOrigins(
                "https://app.yourcompany.com",
                "https://yourcompany.com")
              .AllowAnyMethod()
              .WithHeaders("Authorization", "Content-Type")
              .AllowCredentials());
});

Checklist:

  • AllowAnyOrigin() not used on endpoints that return user-specific or authenticated data
  • AllowAnyOrigin() never combined with AllowCredentials()
  • CORS origin validation uses exact match or properly anchored suffix checks (no string.Contains)
  • Vary: Origin header added when dynamic origin matching is used

Roslyn: Not detected | SAST: Can detect cookie configuration

// ❌ VULNERABLE — session cookie missing security flags
services.AddSession(options =>
{
    options.Cookie.Name = "AppSession";
    // Missing: HttpOnly, Secure, SameSite
});

// ✅ SECURE — fully hardened cookie configuration
services.AddSession(options =>
{
    options.Cookie.Name = "__Session";
    options.Cookie.HttpOnly = true;      // Prevents JS access
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // HTTPS only
    options.Cookie.SameSite = SameSiteMode.Strict; // Prevents CSRF
    options.Cookie.IsEssential = true;
    options.IdleTimeout = TimeSpan.FromMinutes(30);
});

Checklist:

  • Session cookies set HttpOnly = true
  • Session cookies set SecurePolicy = CookieSecurePolicy.Always
  • Session cookies set SameSite = Strict or Lax as appropriate
  • Authentication cookies use the same security flags
  • No sensitive data stored in cookies beyond session identifiers

12. Security Headers

ASP.NET Core does not add security headers by default. Add them via middleware:

// Program.cs — add security headers middleware
app.Use(async (context, next) =>
{
    context.Response.Headers["X-Content-Type-Options"] = "nosniff";
    context.Response.Headers["X-Frame-Options"] = "DENY";
    context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
    context.Response.Headers["Permissions-Policy"] =
        "camera=(), microphone=(), geolocation=()";
    context.Response.Headers["Content-Security-Policy"] =
        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'";
    context.Response.Headers.Remove("Server");
    context.Response.Headers.Remove("X-Powered-By");
    await next();
});

// HSTS (configure in UseHsts or directly)
app.UseHsts(); // Requires HTTPS; adds Strict-Transport-Security header

Checklist:

  • Content-Security-Policy configured and restricts inline scripts
  • Strict-Transport-Security header present with max-age ≥ 31536000
  • X-Content-Type-Options: nosniff present
  • X-Frame-Options: DENY or SAMEORIGIN present
  • Referrer-Policy set to strict-origin-when-cross-origin or stricter
  • Server and X-Powered-By headers removed
  • Debug error pages disabled in production (app.UseExceptionHandler(), not app.UseDeveloperExceptionPage())

13. Sensitive Data Logging

13.1 EF Core Sensitive Data Logging

Roslyn: Not detected | SAST: Can detect with configuration analysis

// ❌ VULNERABLE — logs SQL parameter values including passwords and PII
services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString);
    options.EnableSensitiveDataLogging(); // ← Never in production
});

// ✅ SECURE — sensitive data logging only in development
services.AddDbContext<AppDbContext>((sp, options) =>
{
    options.UseSqlServer(connectionString);

    var env = sp.GetRequiredService<IHostEnvironment>();
    if (env.IsDevelopment())
    {
        options.EnableSensitiveDataLogging();
        options.LogTo(Console.WriteLine, LogLevel.Information);
    }
    // In production: parameters logged as @p0, @p1, never their values
});

Checklist:

  • EnableSensitiveDataLogging() not called in production
  • No logging of raw password strings, API keys, or SSNs
  • Log levels appropriate for production (not LogLevel.Debug globally)
  • PII/health data excluded from structured log fields

14. Authentication and Authorization

Checklist:

  • All controller actions and Razor Pages have explicit [Authorize] or [AllowAnonymous] — no reliance on “security by default”
  • [Authorize] uses role or policy checks — not just authentication check — where privilege levels matter
  • Anti-forgery tokens ([ValidateAntiForgeryToken]) on all state-changing POST endpoints
  • [ApiController] attribute used on API controllers (enables automatic model validation)
  • Password complexity requirements enforced via PasswordOptions in Identity configuration
  • Account lockout enabled via LockoutOptions (prevent brute force)
  • MFA available for admin and privileged accounts
  • JWT tokens validated for iss, aud, exp, and alg claims; alg: none rejected

SAST Tool Configuration for ASP.NET Core

Roslyn Security Rules to Enable as Build Errors

Add to your .csproj:

<PropertyGroup>
  <AnalysisLevel>latest-recommended</AnalysisLevel>
  <RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
  <WarningsAsErrors>
    CA2100;  <!-- SQL injection (ADO.NET, single-method) -->
    CA3001;  <!-- XSS via HttpResponse.Write -->
    CA3002;  <!-- LDAP injection -->
    CA3003;  <!-- File path injection -->
    CA3006;  <!-- OS command injection -->
    CA3007;  <!-- Open redirect -->
    CA5350;  <!-- Weak crypto: TripleDES -->
    CA5351;  <!-- Broken crypto: MD5, DES, RC2 -->
    CA5360;  <!-- Dangerous BinaryFormatter use -->
    CA5369;  <!-- Insecure deserialization: XmlSerializer -->
    CA5394   <!-- Insecure randomness: System.Random -->
  </WarningsAsErrors>
</PropertyGroup>

Roslyn Limitations for ASP.NET Core Security

Roslyn catches obvious single-method patterns. For real enterprise ASP.NET Core applications — where user input flows from a controller action through a service class, through a repository, and into a SqlCommand or FromSqlRaw() call — Roslyn will not fire CA2100 because the dangerous call is not in the same method as the user input.

The table below summarizes what requires a dedicated SAST tool vs. what Roslyn catches:

VulnerabilityRoslynDedicated SAST
SQL injection (same method)✅ CA2100✅ CA2100 + taint
SQL injection (cross-method chain)❌ Missed✅ Taint analysis
Second-order SQL injection❌ Missed✅ DB taint tracking
XSS via @Html.Raw()❌ Missed✅ Razor taint
SSRF via HttpClient❌ Missed✅ Taint analysis
EF Core FromSqlRaw injection❌ Missed✅ EF model
Hardcoded secrets (non-obvious)❌ Missed✅ Pattern + taint
Mass assignment via model binding❌ Missed✅ Binding model
Weak crypto (indirect call)⚠️ Partial✅ Type tracking

For enterprise ASP.NET Core applications, combine Roslyn (fast, in-build feedback) with a dedicated SAST platform (Offensive360) for deep interprocedural taint analysis.


Benchmarking Your SAST Tool Against This Checklist

To evaluate whether your .NET SAST tool reliably detects the vulnerability classes above:

  1. Create test cases for each item — write a minimal ASP.NET Core controller that exhibits the vulnerable pattern across multiple class boundaries
  2. Run your SAST tool and check which findings it reports
  3. Specifically test cross-method injection — if SQL injection that spans a controller → service → repository chain is not found, the tool is using pattern matching, not taint analysis
  4. Test EF Core patternsFromSqlRaw() with string interpolation is the most common EF injection finding; if the tool misses it, .NET EF Core coverage is inadequate
  5. Test second-order SQL injection — stage a write operation followed by a read-then-query sequence; only tools with database taint tracking will find this

If you want to see how Offensive360 SAST performs against your specific ASP.NET Core codebase, a one-time scan for $500 provides a full report within 48 hours — no subscription required.


Frequently Asked Questions

Is Roslyn analysis enough to secure an ASP.NET Core application?

Roslyn analyzers are a valuable first layer — free, integrated into the build, and zero additional configuration for obvious patterns. However, they cannot detect cross-method injection chains, second-order SQL injection, XSS in Razor views via @Html.Raw(), or SSRF. For applications with real security requirements, Roslyn + a dedicated SAST tool is the correct approach.

How do I run this checklist in CI/CD?

Add Roslyn security rules as build errors in your .csproj (see the SAST tool configuration section above). Add a dedicated SAST scan to your GitHub Actions or Azure DevOps pipeline for interprocedural findings. Block PR merges on any Critical findings. See the full CI/CD setup guide for .NET SAST for pipeline examples.

What SAST rules find Entity Framework security issues?

EF Core-specific rules that a capable .NET SAST tool should detect include: FromSqlRaw() called with a string interpolation or concatenation argument (SQL injection), ExecuteSqlRaw() with dynamic input (SQL injection), and entities used directly as model-bound API parameters without DTO filtering (mass assignment). See the EF Core security guide for full details.

Does this checklist apply to .NET Framework (4.x) as well?

Most items apply to both .NET Framework and modern .NET. Differences: BinaryFormatter is deprecated in .NET 5+ but still present in .NET Framework; AesGcm is not available in .NET Framework (use AesCryptoServiceProvider with CipherMode.CBC and a separate HMAC); HSTS middleware differs. A capable .NET SAST tool should support both — verify Framework 4.x support before purchasing.


Offensive360 performs deep interprocedural taint analysis on ASP.NET Core and .NET Framework codebases, detecting all vulnerability classes in this checklist including cross-method injection chains, second-order SQL injection, and EF Core misuse. Run a one-time C# SAST scan for $500 or book a demo to see the full platform.

Offensive360 Security Research Team

Application Security Research

Find vulnerabilities before attackers do

Run Offensive360 SAST and DAST against your applications and get a full vulnerability report in minutes.