top of page

.NET Security Practices That Matter

  • ShiftQuality Contributor
  • Sep 23, 2025
  • 5 min read

Security in .NET applications isn't about paranoia. It's about discipline. The vulnerabilities that get exploited in production aren't exotic zero-days — they're the same mistakes that have been documented for twenty years, showing up in new codebases because developers learned them from tutorials that didn't cover security.

SQL injection. Hardcoded secrets. Missing authorization checks. Exposed stack traces. These aren't theoretical risks. They're the actual attacks that compromise real applications every day.

The good news: .NET has excellent built-in security features. The bad news: you have to use them.

Secrets Management: The First Thing to Get Right

If your application has a connection string, an API key, or a password in a configuration file that's checked into source control, you have a security vulnerability. Full stop.

This is the most common security mistake in .NET applications, and it's one of the easiest to exploit. Anyone with access to your repository — including every developer who's ever had access, every CI/CD system, and potentially the public if the repo is accidentally exposed — can read those secrets.

What to Use Instead

Development: .NET's Secret Manager stores secrets in a user-specific location outside the project directory. They never enter source control.

dotnet user-secrets set "ConnectionStrings:Database" "Server=localhost;Database=myapp;..."
// Accessed the same way as appsettings.json
var connectionString = builder.Configuration.GetConnectionString("Database");

Production: Use your platform's secret management service. Azure Key Vault, AWS Secrets Manager, or HashiCorp Vault. These services encrypt secrets at rest, control access through policies, rotate secrets without code changes, and provide audit logs.

// Azure Key Vault integration
builder.Configuration.AddAzureKeyVault(
    new Uri("https://myvault.vault.azure.net/"),
    new DefaultAzureCredential());

CI/CD: Use your pipeline's secret variables (GitHub Actions secrets, Azure DevOps variables, GitLab CI variables). Never echo secrets in logs. Never pass them as command-line arguments that appear in process listings.

The rule: secrets are never in source control, never in plain text configuration files, and never visible in logs or error messages.

SQL Injection: Still the #1 Web Vulnerability

Entity Framework Core and Dapper both protect against SQL injection — if you use them correctly. The protection comes from parameterized queries, which separate the SQL structure from the user-provided data.

// SAFE — parameterized query via EF Core
var users = await context.Users
    .Where(u => u.Email == userInput)
    .ToListAsync();

// SAFE — parameterized query via Dapper
var users = await connection.QueryAsync<User>(
    "SELECT * FROM Users WHERE Email = @Email",
    new { Email = userInput });

// DANGEROUS — string interpolation in raw SQL
var users = await context.Users
    .FromSqlRaw($"SELECT * FROM Users WHERE Email = '{userInput}'")
    .ToListAsync();

The third example is where developers get burned. FromSqlRaw with string interpolation creates a direct injection vulnerability. Use FromSqlInterpolated instead — it looks identical but properly parameterizes the values.

// SAFE — FromSqlInterpolated parameterizes automatically
var users = await context.Users
    .FromSqlInterpolated($"SELECT * FROM Users WHERE Email = {userInput}")
    .ToListAsync();

The difference between FromSqlRaw with interpolation and FromSqlInterpolated is the difference between a working application and a compromised one. They look almost identical in code. The consequences couldn't be more different.

Authentication and Authorization

Authentication (who are you?) and authorization (what can you do?) are separate concerns, and mixing them up is a common source of vulnerabilities.

Authentication

For most .NET web applications, don't build your own authentication. Use ASP.NET Core Identity for basic username/password auth, or integrate with an external provider (Azure AD, Auth0, Okta) for SSO and enterprise requirements.

If you must handle passwords, use ASP.NET Core Identity's built-in password hasher, which uses PBKDF2 with a random salt. Never store passwords in plain text. Never use MD5 or SHA-256 for password hashing — they're fast hash algorithms, and fast is the opposite of what you want for passwords.

Authorization: The Check People Forget

Authorization failures are more common than authentication failures. The pattern: a developer adds an [Authorize] attribute to require login, but doesn't check whether the logged-in user should access the specific resource.

// VULNERABLE — any authenticated user can view any order
[Authorize]
[HttpGet("{orderId}")]
public async Task<IActionResult> GetOrder(int orderId)
{
    var order = await context.Orders.FindAsync(orderId);
    return Ok(order);
}

// SECURE — verifies the order belongs to the requesting user
[Authorize]
[HttpGet("{orderId}")]
public async Task<IActionResult> GetOrder(int orderId)
{
    var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    var order = await context.Orders
        .FirstOrDefaultAsync(o => o.Id == orderId && o.UserId == userId);

    if (order == null) return NotFound();
    return Ok(order);
}

This is called an Insecure Direct Object Reference (IDOR). It's consistently in the OWASP Top 10 because it's easy to miss — the endpoint "works" for the happy path, and the vulnerability only matters when someone changes the ID in the URL to access someone else's data. Which they will.

Policy-Based Authorization

For complex authorization rules, use ASP.NET Core's policy system rather than scattering role checks throughout your code.

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CanManageTeam", policy =>
        policy.RequireRole("Admin", "TeamLead"));
});

[Authorize(Policy = "CanManageTeam")]
[HttpPost("team/members")]
public async Task<IActionResult> AddTeamMember(/*...*/) { /*...*/ }

Policies centralize authorization logic. When the rules change, you update one policy definition rather than hunting through every controller for role checks.

Cross-Site Scripting (XSS)

Razor views and Blazor components HTML-encode output by default. This means user-provided content like <script>alert('hacked')</script> renders as text, not executable code.

The vulnerability appears when you bypass this protection:

// SAFE — Razor encodes by default
<p>@userInput</p>

// DANGEROUS — Html.Raw disables encoding
<p>@Html.Raw(userInput)</p>

Html.Raw should only be used with content you control — never with user input. If you need to render user-provided HTML (like a rich text editor), sanitize it with a library like HtmlSanitizer before rendering.

Error Handling: Don't Leak Information

A stack trace in a production error response tells an attacker which framework you're using, which libraries are installed, your file paths, and sometimes your database schema. This is free reconnaissance.

// In Program.cs — different behavior per environment
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/error");
}

In production, return generic error messages to the client and log the detailed error server-side. The user sees "Something went wrong." Your logging system sees the full stack trace.

HTTPS Everywhere

Enforce HTTPS for all traffic. ASP.NET Core makes this straightforward:

builder.Services.AddHsts(options =>
{
    options.MaxAge = TimeSpan.FromDays(365);
    options.IncludeSubDomains = true;
});

app.UseHttpsRedirection();
app.UseHsts();

HSTS (HTTP Strict Transport Security) tells browsers to always use HTTPS for your domain, preventing downgrade attacks. UseHttpsRedirection redirects any HTTP request to HTTPS.

The Security Checklist

Before any .NET application goes to production:

  • No secrets in source control or configuration files

  • All database queries use parameterized queries

  • Authorization checks verify resource ownership, not just authentication

  • Error responses don't leak stack traces or internal details

  • HTTPS is enforced with HSTS

  • User input is never rendered as raw HTML without sanitization

  • Dependencies are updated (check dotnet list package --vulnerable)

  • CORS is configured to allow only expected origins

This isn't exhaustive. But it covers the vulnerabilities that actually get exploited in production .NET applications — the ones that show up in breach reports, not in theoretical security papers.

Key Takeaway

.NET security that matters is about discipline, not complexity. Manage secrets properly, use parameterized queries, check authorization at the resource level, don't leak error details, enforce HTTPS, and keep dependencies updated. The framework gives you the tools — you just have to use them.

This completes the .NET Beyond Basics learning path. You've covered production patterns, testing at scale, performance profiling, and security practices. The throughline: production .NET is about the things tutorials skip — the patterns that keep applications reliable, fast, and secure under real conditions.

Comments


bottom of page