ASP.NET Core  

How to Perform Security Testing on ASP.NET Core Applications

1. Overview & Approach

Security testing for ASP.NET Core should include multiple layers:

  1. Static Analysis (SAST): scan code for insecure patterns.

  2. Dependency/Package Scanning: find vulnerable NuGet packages.

  3. Configuration Review: Ensure framework and middleware settings are secure.

  4. Dynamic Testing (DAST): exercise the running app to find runtime flaws.

  5. Automated Integration Tests: programmatic tests that assert security properties (headers, auth, CSRF, cookie flags).

  6. Manual Pen-testing: targeted manual checks for XSS, SQLi, auth/authorization bypass.

  7. 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

  • UseAuthentication() must come before UseAuthorization().

  • Keep secrets out of appsettings.json in production; use environment variables or Azure Key Vault / AWS Secrets Manager.

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:

  • Check vulnerable NuGet packages:

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.