1. Overview & Approach
Security testing for ASP.NET Core should include multiple layers:
Static Analysis (SAST): scan code for insecure patterns.
Dependency/Package Scanning: find vulnerable NuGet packages.
Configuration Review: Ensure framework and middleware settings are secure.
Dynamic Testing (DAST): exercise the running app to find runtime flaws.
Automated Integration Tests: programmatic tests that assert security properties (headers, auth, CSRF, cookie flags).
Manual Pen-testing: targeted manual checks for XSS, SQLi, auth/authorization bypass.
CI/CD enforcement: run security checks automatically.
This article focuses on C# code for configuration checks, middleware to enforce secure practices, and integration tests that can be added to CI.
2. Secure Configuration (Program.cs / Startup.cs)
Make sure your app has these basics in Program.cs
(or Startup.cs
for older templates):
// Program.cs (.NET 6 or later)
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddAuthentication("Cookies")
.AddCookie("Cookies", options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax; // or Strict where feasible
options.LoginPath = "/Account/Login";
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
});
// Add Antiforgery
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
options.Cookie.Name = "XSRF-TOKEN";
options.Cookie.HttpOnly = false; // client JS may need token
});
var app = builder.Build();
// Enforce HTTPS and HSTS
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts(); // Strict-Transport-Security header
}
app.UseHttpsRedirection();
// Security headers middleware (see next section)
app.UseMiddleware<SecurityHeadersMiddleware>();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapRazorPages();
app.Run();
Notes
3. Security Headers Middleware (C# ready)
Add a small middleware to assert security headers and add defaults. Drop this into your project:
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
public class SecurityHeadersMiddleware
{
private readonly RequestDelegate _next;
public SecurityHeadersMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
var headers = context.Response.Headers;
// Prevent clickjacking
if (!headers.ContainsKey("X-Frame-Options"))
headers["X-Frame-Options"] = "DENY";
// Prevent MIME-sniffing
if (!headers.ContainsKey("X-Content-Type-Options"))
headers["X-Content-Type-Options"] = "nosniff";
// Basic CSP — adapt for your app (don't use 'unsafe-inline' in production)
if (!headers.ContainsKey("Content-Security-Policy"))
headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self' 'unsafe-inline'; object-src 'none'; frame-ancestors 'none';";
// Referrer policy
if (!headers.ContainsKey("Referrer-Policy"))
headers["Referrer-Policy"] = "no-referrer";
// Feature-Policy / Permissions-Policy (modern replacement)
if (!headers.ContainsKey("Permissions-Policy"))
headers["Permissions-Policy"] = "geolocation=(), microphone=()";
await _next(context);
}
}
Register it in Program.cs
as shown earlier.
4. Input Validation & Model Binding
Prefer strongly typed models and validation attributes. Never trust user input.
public class CreateUserDto
{
[Required]
[StringLength(100, MinimumLength = 3)]
public string Username { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
// Avoid binding secret fields directly from the client if possible
[Required]
[MinLength(8)]
public string Password { get; set; }
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult CreateUser([FromForm] CreateUserDto dto)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
// proceed (hash password with a secure algorithm e.g. PBKDF2/Argon2/BCrypt)
return Ok();
}
Server-side rules
Use parameterized queries or EF Core LINQ. Do not concatenate SQL strings.
For EF Core raw SQL, use parameters: dbContext.Database.ExecuteSqlRaw("UPDATE ... WHERE Id = {0}", id);
5. File Upload Security (example)
Validate file type, size, and store outside web root (or in blob storage). Example:
[HttpPost]
[RequestSizeLimit(5 * 1024 * 1024)] // 5 MB
public async Task<IActionResult> Upload(IFormFile file)
{
if (file == null || file.Length == 0) return BadRequest("Empty file.");
// Basic extension check
var permittedExtensions = new[] { ".jpg", ".png", ".pdf" };
var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
return BadRequest("Invalid file type.");
// Optionally validate content-type and check file signature bytes
// Save to safe location
var filePath = Path.Combine("/var/uploads", $"{Guid.NewGuid()}{ext}");
using (var stream = System.IO.File.Create(filePath))
{
await file.CopyToAsync(stream);
}
return Ok();
}
Recommendation: run antivirus/clamAV scanning on uploaded files and do content sniffing to prevent disguised executables.
6. Anti-Forgery (CSRF)
For web forms, use @Html.AntiForgeryToken()
in Razor views and decorate POST action with [ValidateAntiForgeryToken]
. For single-page apps, send the antiforgery token via a header (X-CSRF-TOKEN
) and validate on the server (see AddAntiforgery
in Program.cs).
7. Programmatic Security Tests (Integration Tests with C#)
Use Microsoft.AspNetCore.Mvc.Testing
and xUnit
to write integration tests that assert security headers, cookie flags, and auth/authorization behavior.
Install:
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package xunit
dotnet add package FluentAssertions
Example test project:
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
public class SecurityIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public SecurityIntegrationTests(WebApplicationFactory<Program> factory)
{
// Use a factory configured for testing environment
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
[Fact]
public async Task Root_Returns_SecurityHeaders()
{
var res = await _client.GetAsync("/");
res.StatusCode.Should().Be(HttpStatusCode.OK);
res.Headers.Contains("X-Frame-Options").Should().BeTrue();
res.Headers.Contains("X-Content-Type-Options").Should().BeTrue();
res.Headers.Contains("Content-Security-Policy").Should().BeTrue();
res.Headers.Contains("Referrer-Policy").Should().BeTrue();
}
[Fact]
public async Task Cookie_Is_HttpOnly_And_Secure()
{
// An endpoint that sets auth cookie or any test cookie
var res = await _client.GetAsync("/account/set-test-cookie");
res.StatusCode.Should().Be(HttpStatusCode.OK);
var setCookie = res.Headers.GetValues("Set-Cookie").FirstOrDefault();
setCookie.Should().NotBeNull();
setCookie.Should().Contain("HttpOnly");
setCookie.Should().Contain("Secure");
}
[Fact]
public async Task PostWithoutAntiforgery_IsRejected()
{
var content = new FormUrlEncodedContent(new Dictionary<string, string> { { "dummy", "value" } });
var res = await _client.PostAsync("/account/createuser", content);
// depending on config, it might be 400 BadRequest or 403 Forbidden
res.StatusCode.Should().BeOneOf(HttpStatusCode.BadRequest, HttpStatusCode.Forbidden);
}
}
Note: You may need to stub or add a testing-only endpoint /account/set-test-cookie
in your app to exercise cookie-related behavior.
8. Automated Dependency & Secret Checks
Add these scripts/commands to your build pipeline to detect package vulnerabilities and accidental secrets:
dotnet list package --vulnerable
Simple scan for obvious secrets (CI job example using git-secrets
or truffleHog
— not shown here as those are external tools, but call them in your pipeline).
Use GitHub Dependabot / Snyk/WhiteSource for continuous monitoring.
9. SAST & Code Analysis
Use Roslyn analyzers (e.g., Microsoft.CodeAnalysis.FxCopAnalyzers) and add them to your Directory.Build.props
so CI fails on critical issues.
Integrate SonarQube or similar SAST tools into the build.
Example Directory.Build.props
to enable analyzer warnings-as-errors:
<Project>
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors>CA2007;CA1303</WarningsAsErrors>
</PropertyGroup>
</Project>
10. Example Dynamic Tests You Can Automate From C#
If you want to drive OWASP ZAP from C# to do DAST scans, you can call the ZAP API or run it as a Docker process and call its HTTP API. (Below is a simplified example that triggers an external process — adapt to your CI):
using System.Diagnostics;
public static void RunZapSpider(string targetUrl)
{
var psi = new ProcessStartInfo
{
FileName = "docker",
Arguments = $"run --rm -v $(pwd):/zap/wrk -t owasp/zap2docker-stable zap-baseline.py -t {targetUrl}",
RedirectStandardOutput = true,
UseShellExecute = false
};
var p = Process.Start(psi);
p.WaitForExit();
var output = p.StandardOutput.ReadToEnd();
Console.WriteLine(output);
}
Tip: Running ZAP requires setup; many teams run a staged ZAP scan in CI that publishes a report artifact.
11. Test Cases / Checklist (Copy-paste friendly)
Add these as unit/integration tests or a manual testing checklist:
Authentication: brute force prevention (rate limiting, CAPTCHA, lockout).
Authorization: test endpoints with lower-privileged tokens and assert 403
.
CSRF: POST endpoints require a valid antiforgery token.
XSS: Try stored and reflected XSS vectors in inputs and verify output encoding.
SQL/command injection: ensure parameterized queries.
File uploads: validate extension, file signature, size, and scanning.
Cookies: HttpOnly
, Secure
, SameSite
set.
Security headers: CSP
, X-Frame-Options
, X-Content-Type-Options
, Referrer-Policy
, Permissions-Policy
.
TLS: redirect HTTP->HTTPS and HSTS enabled in production.
Secrets: no secrets in repo or config.
Dependencies: dotnet list package --vulnerable
passes.
Logging: no PII or secrets logged.
Rate limiting: endpoints protected (e.g., IP/credential throttling).
Error messages: do not leak stack traces or sensitive info in production.
12. CI Example (GitHub Actions) — run tests + vulnerable packages check
.github/workflows/security.yml
(simple example):
name: Security Checks
on:
push:
branches: [ main ]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Run tests
run: dotnet test --no-build --verbosity normal
- name: Check vulnerable packages
run: |
dotnet list package --vulnerable
Adjust dotnet-version to your target runtime.
13. Manual Pen-Testing Pointers (Practical C#-oriented)
Use Burp Suite or OWASP ZAP to crawl pages; export the session and re-run against staging.
Use HttpClient
in C# to fuzz endpoints programmatically (send malformed JSON, unexpected types).
Test for mass-assignment issues: send extra JSON properties to see if the server binds unintended properties.
Example quick fuzzer snippet
using System.Net.Http.Json;
public async Task FuzzEndpoints(HttpClient client, string endpoint)
{
var fuzzPayloads = new[] {
new { username = "' OR 1=1 --", password = "x" },
new { username = "<script>alert(1)</script>", password = "x" },
new { username = new string('A', 10000), password = "x" }
};
foreach (var p in fuzzPayloads)
{
var res = await client.PostAsJsonAsync(endpoint, p);
Console.WriteLine($"Payload length {JsonSerializer.Serialize(p).Length} => {res.StatusCode}");
}
}
14. Reporting & Remediation
For each finding, include: title, severity (Low/Med/High/Critical), reproduction steps, sample request/response, and remediation suggestions (code snippets when possible).
Track fixes and re-test automatically using the integration tests you added above.
15. Quick Reference: Secure Defaults (Checklist to enforce in code)
UseHttpsRedirection()
enabled.
UseHsts()
in non-Dev environments.
Cookie.HttpOnly = true
, Cookie.SecurePolicy = Always
, SameSite
appropriate.
ValidateAntiForgeryToken
on POST / state-changing endpoints.
No plaintext secrets checked into source.
CSP header present and tuned for your app.
Content type validation and X-Content-Type-Options: nosniff
.
Authorization policies applied to controllers/actions.
Error handling middleware that hides stack traces in production.
16. Wrapping Up
This guide gave you:
C# middleware to apply security headers,
Example secure Program.cs
wiring,
Model validation & file upload example,
Integration tests for headers, cookies, and antiforgery,
CI steps to run tests and check vulnerable packages,
Practical fuzzing and DAST integration hints.