Skip to content
The OWASP Top 10 for .NET Developers: A Practical Guide to Mitigating Critical Web App Risks

The OWASP Top 10 for .NET Developers: A Practical Guide to Mitigating Critical Web App Risks

1 Introduction: Modern Security Landscape and the .NET Architect

In the evolving digital landscape, securing web applications has become more critical—and more challenging—than ever. As a software architect building applications with .NET, security must be embedded in your approach, much like performance or scalability.

But have you ever paused to consider how secure your architecture really is? Let’s unpack this together.

1.1 Beyond the Basics: Why Security is an Architectural Concern

Many developers treat security as a last-minute addition, a patch applied just before deployment. But this mindset can introduce severe vulnerabilities. Instead, security should be architected from day one—woven into the fabric of your design, much like steel reinforcement in concrete buildings.

Think of it this way: You wouldn’t build a skyscraper without a solid foundation designed specifically to withstand earthquakes, wind, and heavy usage, right? Similarly, your .NET applications must be structurally sound from a security perspective, capable of resisting cyber threats and breaches.

1.2 What is OWASP and Why the Top 10 Matters for .NET Applications

OWASP, or the Open Web Application Security Project, is a globally recognized organization dedicated to improving software security. Its annual OWASP Top 10 highlights the most critical security risks affecting web applications today.

But why should this matter to you as a .NET developer?

The OWASP Top 10 isn’t just a theoretical list—it’s a roadmap of practical concerns and actionable recommendations that significantly enhance your application’s resilience. Ignoring it is akin to disregarding safety standards in engineering. Sure, your building may stand initially, but will it withstand unforeseen attacks?

1.3 A Practical Approach: Moving from Theory to Secure-by-Design Architecture

Throughout this guide, we’ll explore the OWASP Top 10 specifically tailored for the modern .NET developer. We’ll go beyond concepts, diving deep into practical architectural strategies and code examples using C#, ASP.NET Core, Entity Framework, Azure services, and best practices.

By the end, you’ll be equipped to proactively mitigate these risks, implementing a secure-by-design architecture that integrates seamlessly into your existing workflows.


2 A01:2021 - Broken Access Control

2.1 Defining the Risk: The Chasm Between Authentication and Authorization in .NET

Authentication verifies who a user is, while authorization determines what that user is allowed to do. It’s tempting to treat these as identical—but that’s precisely where vulnerabilities occur. Broken access control vulnerabilities arise when applications fail to enforce restrictions properly, granting unauthorized access to sensitive data or operations.

Imagine authentication as your office ID badge—it gets you through the front door. Authorization, however, determines which rooms your badge can open. Misconfiguring these permissions exposes sensitive areas to anyone with an ID, creating a critical security gap.

2.2 Common Pitfalls in ASP.NET Core Applications

2.2.1 Insecure Direct Object References (IDOR) with Entity Framework Core

One of the common pitfalls is exposing internal references directly in URLs. Consider the following insecure API endpoint:

[HttpGet("orders/{id}")]
public IActionResult GetOrder(int id)
{
    var order = _context.Orders.Find(id);
    return Ok(order);
}

The problem here is that any user can access any order if they guess or brute-force the order ID.

2.2.2 Missing Function-Level Access Control in Minimal APIs and MVC Controllers

Minimal APIs, introduced with .NET 6, simplify endpoint creation. But simplicity shouldn’t mean overlooking security. A common issue is failing to enforce access restrictions explicitly.

For example:

app.MapDelete("/users/{id}", (int id) => DeleteUser(id));

Without explicit authorization checks, this endpoint is wide open to abuse.

2.2.3 Misconfiguration of CORS (Cross-Origin Resource Sharing)

Misconfigured CORS policies inadvertently open doors to cross-origin attacks. A loosely defined policy can allow unauthorized domains access:

services.AddCors(options =>
{
    options.AddPolicy("OpenPolicy",
        builder => builder.AllowAnyOrigin()
                          .AllowAnyMethod()
                          .AllowAnyHeader());
});

While convenient for testing, this configuration is risky for production environments.

2.3 Architecting for Robust Access Control

2.3.1 The Principle of Least Privilege in Practice

Always assign the minimum necessary permissions required for each user or component to perform their tasks. This approach limits the damage potential if credentials are compromised.

2.3.2 Implementing Role-Based Access Control (RBAC) with ASP.NET Core Identity

RBAC simplifies management by assigning permissions based on roles:

[Authorize(Roles = "Admin")]
public IActionResult AdminPanel()
{
    return View();
}

But for complex applications, roles alone may not suffice.

2.3.3 Advanced Scenarios: Policy-Based and Claims-Based Authorization

Policy-based authorization allows granular access control based on custom rules. Claims-based authorization further enhances granularity by defining permissions through specific claims.

2.3.4 Code Example: Crafting Custom Authorization Handlers for Fine-Grained Control

Consider a custom authorization policy handler:

public class DocumentAuthorizationHandler : AuthorizationHandler<EditRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, 
        EditRequirement requirement, 
        Document document)
    {
        if (context.User.HasClaim("DocumentEditor", document.OwnerId.ToString()))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }
}

This handler precisely checks whether the current user has editing rights based on ownership.

2.4 Mitigation Checklist for Architects

  • Implement the principle of least privilege.
  • Avoid exposing internal identifiers directly (use GUIDs or indirect identifiers).
  • Enforce strict CORS policies with specific, allowed origins.
  • Adopt RBAC and policy-based authorization for granular control.
  • Regularly audit access control logic and authorization handlers.

3 A02:2021 - Cryptographic Failures

3.1 Defining the Risk: The Dangers of Weak or Improper Cryptography in .NET

Cryptographic failures occur when applications incorrectly implement encryption, use outdated algorithms, or mismanage cryptographic keys, leading to sensitive data exposure.

Consider cryptography as a lock on your door. An old, rusty lock (weak cryptography) or an improperly installed lock (mismanaged cryptography) is equally ineffective against intruders.

3.2 Sensitive Data Exposure: At Rest, In Transit, and In Memory

3.2.1 At Rest: Transparent Data Encryption (TDE) in SQL Server and Azure SQL

Encrypting data at rest ensures it remains secure even if storage systems are compromised. Azure SQL Database and SQL Server offer TDE to encrypt data seamlessly at the file level.

3.2.2 In Transit: Enforcing HTTPS, HSTS, and TLS 1.2/1.3 in Kestrel and IIS

Enforce secure communication protocols by configuring your .NET applications and servers to use HTTPS with modern TLS versions.

3.2.3 In Memory: Protecting Secrets with ProtectedData and Azure Key Vault

Sensitive data in memory is vulnerable to exploits. Using tools like ProtectedData APIs or Azure Key Vault mitigates this risk by managing secrets securely.

3.3 Practical Implementation

3.3.1 Code Example: Using System.Security.Cryptography

Here’s symmetric encryption using AES-GCM:

using var aes = new AesGcm(key);
aes.Encrypt(nonce, plaintext, ciphertext, tag);

For asymmetric encryption (RSA):

using var rsa = RSA.Create(2048);
var encrypted = rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256);

3.3.2 Architectural Pattern: Secrets-as-a-Service with Azure Key Vault

Using Azure Key Vault with Managed Identities simplifies secret management securely:

var client = new SecretClient(new Uri("https://your-vault.vault.azure.net/"), new DefaultAzureCredential());
var secret = await client.GetSecretAsync("ConnectionString");

3.4 Mitigation Checklist for Architects

  • Encrypt data at rest, in transit, and in memory.
  • Use secure algorithms (AES-GCM, RSA with OAEP).
  • Regularly rotate and manage keys with Azure Key Vault.
  • Enable HSTS and enforce TLS 1.2 or higher.
  • Avoid homegrown cryptographic implementations—always rely on established libraries.

4 A03:2021 – Injection

4.1 Defining the Risk: More Than Just SQL Injection

When you hear the word “injection,” what comes to mind? For many, SQL injection remains the poster child. But the landscape of injection attacks is far broader and more nuanced, particularly for .NET developers.

Injection refers to the tricking of an interpreter—whether it’s SQL, a shell, or even a browser—into executing unintended commands by inserting untrusted data into a program. Attackers exploit applications that naively pass user-supplied input to interpreters without sufficient validation or sanitization. In essence, injection is about losing control over your application’s instructions and letting external input shape its behavior in unpredictable and often dangerous ways.

For .NET architects, ignoring injection is like letting strangers dictate the next steps in your application’s workflow. The risks include unauthorized data access, data corruption, privilege escalation, and even full system compromise.

Let’s break down the most relevant injection flaws in the .NET ecosystem—and how you can defend your architecture.

4.2 A Deep Dive into Common .NET Injection Flaws

4.2.1 SQL Injection: The Lingering Threat and its Mitigation with Entity Framework Core

Despite years of warnings, SQL injection continues to surface in production systems. Why? Because whenever raw SQL queries are constructed with string concatenation or unsanitized input, the door remains open. Consider this classic anti-pattern:

var query = $"SELECT * FROM Users WHERE Username = '{username}'";
var users = dbContext.Users.FromSqlRaw(query).ToList();

If an attacker supplies a cleverly crafted username, the query’s logic can be hijacked, exposing or manipulating data.

Entity Framework Core to the Rescue

Modern .NET development has no excuse for this risk. Entity Framework Core, by design, parameterizes queries. For example:

var users = dbContext.Users
    .Where(u => u.Username == username)
    .ToList();

Even when using raw SQL, always use parameters:

var users = dbContext.Users
    .FromSqlInterpolated($"SELECT * FROM Users WHERE Username = {username}")
    .ToList();

This approach escapes the supplied values, closing off the injection vector.

What about Stored Procedures?

Even stored procedures can be vulnerable if parameters are mishandled. Use parameterized APIs—not string concatenation.

4.2.2 Cross-Site Scripting (XSS): Reflected, Stored, and DOM-based XSS in Razor Pages and Blazor

Not all injection flaws target your data layer. Cross-Site Scripting (XSS) allows attackers to inject malicious scripts into web pages viewed by users. In .NET, XSS commonly arises in Razor Pages, MVC Views, and—more recently—Blazor apps.

Reflected XSS

Reflected XSS occurs when user input is echoed back in server responses without proper encoding. For example:

return Content($"<h1>Welcome, {userInput}</h1>", "text/html");
Stored XSS

Stored XSS arises when malicious content is saved to a database and then rendered for all users, as with comment fields or user profiles.

DOM-based XSS

This variation is less about the server, more about unsafe handling of user input within JavaScript on the client side—even in Blazor apps, especially if integrating JS components.

Razor and Blazor Default Protections

Razor provides automatic HTML encoding when using the @ syntax. However, using @Html.Raw() or other output without encoding reintroduces risk.

@Model.UserInput // Safe, automatically encoded
@Html.Raw(Model.UserInput) // Dangerous, use only with trusted data

4.2.3 OS Command Injection and LDAP Injection

It’s not only databases and browsers that need protection. Any system call or interpreter can be abused.

OS Command Injection

Suppose you run a system process using untrusted input:

var psi = new ProcessStartInfo("cmd.exe", $"/C dir {userInput}");
Process.Start(psi);

An attacker could append & del *.* or worse. Never concatenate user input into OS commands.

LDAP Injection

The same principle applies with LDAP queries:

string filter = $"(uid={userInput})";
var search = new DirectorySearcher(filter);

User input can manipulate the LDAP filter, leading to unauthorized data exposure.

4.3 Architecting a Defense-in-Depth Strategy

4.3.1 The Power of Parameterized Queries and ORMs (Entity Framework)

ORMs like Entity Framework abstract away much of the danger. They parameterize queries by default, preventing SQL injection. When raw SQL is necessary, always use APIs that parameterize input (such as FromSqlInterpolated or explicit command parameters).

4.3.2 Output Encoding: Leveraging ASP.NET Core’s Built-in Encoders

Never trust user input for output. Use the built-in encoders:

  • Razor syntax encodes output by default.
  • Use System.Text.Encodings.Web for manual encoding if building output strings.
  • Only use Html.Raw or similar methods for data you know is safe.

4.3.3 Implementing a Strict Content Security Policy (CSP)

A robust CSP header helps mitigate XSS even if an injection slips through. For example:

app.Use(async (context, next) =>
{
    context.Response.Headers.Add("Content-Security-Policy", "default-src 'self'; script-src 'self';");
    await next();
});

This policy prevents most inline scripts and only allows scripts from your domain.

4.4 Mitigation Checklist for Architects

  • Use parameterized queries or ORMs for all data access
  • Output encode all untrusted data in views and APIs
  • Never execute OS commands or LDAP queries with untrusted input
  • Apply a strict Content Security Policy header
  • Audit for unsafe use of Html.Raw, string concatenation, and third-party JS libraries
  • Educate development teams on the full spectrum of injection attacks

5 A04:2021 – Insecure Design

5.1 Defining the Risk: When Security is an Afterthought

You can’t “bolt on” security at the end. Insecure design means fundamental weaknesses are baked in from the start—missing security controls, poor trust boundaries, or risky default behaviors.

Think of it as building a house with beautiful finishes but neglecting to lock the doors or reinforce the windows. Fixing these gaps later is costly, and sometimes impossible.

For .NET architects, secure design means anticipating threats and weaving mitigations into the application’s structure from day one.

5.2 Threat Modeling for .NET Architects

5.2.1 Introduction to the STRIDE Methodology

Threat modeling helps you proactively identify potential security flaws before you build. STRIDE is a simple but powerful methodology, breaking down threats into six categories:

  • Spoofing: Impersonation or identity subversion
  • Tampering: Data manipulation in transit or at rest
  • Repudiation: Actions that can’t be traced or proven
  • Information Disclosure: Leaking sensitive data
  • Denial of Service: Disrupting service availability
  • Elevation of Privilege: Gaining unauthorized permissions

STRIDE lets you systematically walk through application flows, spotting risks and planning mitigations.

5.2.2 A Practical Example: Threat Modeling a .NET E-commerce Checkout Process

Consider a checkout process. Applying STRIDE:

  • Spoofing: Could someone fake their identity or use another’s session?
  • Tampering: Can an attacker alter the order amount or product IDs?
  • Repudiation: Are critical actions logged? Can customers claim they didn’t make a purchase?
  • Information Disclosure: Is payment data exposed in logs or URLs?
  • Denial of Service: Are there limits to prevent bulk order submissions?
  • Elevation of Privilege: Could a user escalate privileges to access admin functions?

This approach surfaces risks before code is written. Now, security becomes part of your acceptance criteria, not a retrofit.

5.3 Secure Design Patterns

5.3.1 The Secure Defaults Principle in Action

Applications should be secure by default. For example, APIs should require authentication by default, not as an optional add-on. Input validation and output encoding should be enabled everywhere.

Example: In ASP.NET Core, you can require authorization globally:

services.AddControllers(config =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
    config.Filters.Add(new AuthorizeFilter(policy));
});

5.3.2 Separation of Duties in Microservices Architecture

Modern .NET solutions often use microservices. Each service should have only the permissions it needs, and services should not share secrets or infrastructure without strict controls.

For example, a payment service shouldn’t have access to customer service data. Use Azure Managed Identities or similar IAM (Identity and Access Management) solutions to enforce separation at the cloud infrastructure level.

5.4 Mitigation Checklist for Architects

  • Adopt threat modeling at the start of every major project or feature
  • Use secure defaults: require authentication, validate input, and minimize permissions
  • Design for separation of duties—at both application and infrastructure levels
  • Document security controls in architecture diagrams and specifications
  • Review and update designs regularly as threats evolve

6 A05:2021 – Security Misconfiguration

6.1 Defining the Risk: The Unlocked Doors in Your .NET Stack

No matter how strong your application’s logic, misconfiguration can quietly open the backdoor. Security misconfiguration is the catch-all category for default settings, forgotten debug endpoints, unnecessary services, verbose error messages, and anything else that wasn’t locked down before go-live.

Think of it like forgetting to change the default admin password or leaving your cloud storage bucket world-readable. These mistakes are common and, unfortunately, often missed by automated scans.

6.2 Identifying Common Misconfigurations

6.2.1 Verbose Error Messages in Production (ASPNETCORE_ENVIRONMENT)

During development, verbose errors help diagnose problems. In production, these messages can expose sensitive information—connection strings, stack traces, software versions—to attackers.

Best Practice: Ensure ASPNETCORE_ENVIRONMENT is set to Production in production deployments and use user-friendly error pages.

6.2.2 Default Accounts and Passwords

Out-of-the-box accounts are a hacker’s best friend. Many breaches begin with default admin accounts that were never disabled or renamed.

Best Practice: Remove, disable, or change default credentials. Enforce password complexity and multi-factor authentication wherever possible.

6.2.3 Unpatched Frameworks: The Importance of Updating NuGet Packages

Old libraries and frameworks contain known vulnerabilities. Attackers actively scan for services running outdated versions.

Best Practice: Regularly update all dependencies. Use tools like dotnet list package --vulnerable and monitor your package feeds for security advisories.

6.2.4 Misconfigured HTTP Headers (X-Frame-Options, X-Content-Type-Options, etc.)

Secure HTTP headers can prevent common web attacks like clickjacking, MIME-sniffing, and caching of sensitive data. Misconfiguration, or lack thereof, leaves your application exposed.

6.3 Strategies for Hardening Your .NET Environment

6.3.1 Using Middleware to Set Secure HTTP Headers

In ASP.NET Core, use middleware to set headers globally:

app.Use(async (context, next) =>
{
    context.Response.Headers.Add("X-Frame-Options", "DENY");
    context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
    context.Response.Headers.Add("Referrer-Policy", "no-referrer");
    await next();
});

Consider using the NWebsec library for advanced header management.

6.3.2 Automating Dependency Scanning with dotnet list package --vulnerable and GitHub Dependabot

Automate security in your CI/CD pipeline. Use:

  • dotnet list package --vulnerable to scan for known issues
  • GitHub Dependabot for automatic pull requests when vulnerabilities are found

Automated scanning helps you keep pace with emerging threats without manual effort.

6.3.3 Configuration Management: appsettings.json vs. Environment Variables vs. Azure App Configuration

Never hard-code secrets or sensitive configuration values. Prefer environment variables or managed services like Azure App Configuration or Azure Key Vault.

Example: Configure connection strings via environment variables in Startup.cs:

builder.Configuration.AddEnvironmentVariables();

Or, use Azure App Configuration:

builder.Configuration.AddAzureAppConfiguration("<connection-string>");

6.4 Mitigation Checklist for Architects

  • Set ASPNETCORE_ENVIRONMENT to Production and use user-friendly error pages
  • Remove or change default accounts and passwords
  • Regularly scan for and update vulnerable packages
  • Set secure HTTP headers using middleware
  • Manage secrets and sensitive configs outside of source code (environment variables, Azure services)
  • Harden infrastructure—disable unused ports, services, and debug endpoints

7 A06:2021 – Vulnerable and Outdated Components

7.1 Defining the Risk: The Hidden Dangers in Your csproj File

Every .NET architect has stared at a lengthy list of NuGet package references inside a csproj file. It’s easy to focus on what those libraries deliver—JSON parsing, logging, authentication, or data access—and forget about what they might bring along: vulnerabilities and outdated dependencies.

Attackers know that applications often inherit risks from their dependencies. The more third-party code you rely on, the greater your attack surface. A single unpatched or compromised library can undermine the security of an otherwise robust application, sometimes bypassing even your strongest defenses.

How often do you review your dependencies, both direct and transitive? And do you track security advisories for each of them? If not, you’re leaving a blind spot in your architecture.

7.2 The NuGet Ecosystem: A Double-Edged Sword

NuGet has revolutionized .NET development by making it simple to incorporate third-party libraries and keep them up to date. Yet, this convenience is a double-edged sword. With over 350,000 packages available, not all are actively maintained, well-vetted, or free from vulnerabilities.

7.2.1 Transitive Dependencies: The Risks You Don’t See

It’s not just about the packages you explicitly install. Every NuGet package can bring a network of its own dependencies, called transitive dependencies. You may not even be aware of all the libraries loaded into your app at runtime.

For example, installing a single logging framework could bring in half a dozen supporting packages. If any one of those has a vulnerability—such as an unsafe version of a cryptography library or a deserializer with a remote code execution bug—your application inherits the risk.

How to Audit Your Dependency Graph

Use the built-in .NET CLI to visualize and audit your dependencies:

dotnet list package --include-transitive

This command helps you understand the full scope of what your application is shipping.

Take the case of Newtonsoft.Json, widely used for JSON serialization in .NET apps. In 2017, a vulnerability was discovered that allowed certain crafted payloads to cause denial of service through excessive memory allocation. Many teams were using Newtonsoft.Json indirectly via other packages and weren’t even aware they were exposed.

Vulnerabilities like this highlight the importance of monitoring and updating all dependencies—not just those you add directly.

7.3 Building a Component Governance Strategy

As an architect, you need a plan to govern the use of third-party components. This strategy should be practical, automated where possible, and part of your development workflow.

7.3.1 Establishing a Vetting Process for New Libraries

Not all packages are created equal. Before introducing a new dependency:

  • Check the source: Is it open source? Who maintains it? Is the repo active?
  • Review issues and pull requests: Are vulnerabilities disclosed and fixed promptly?
  • Evaluate documentation: Well-maintained packages have thorough docs and security advisories.
  • Prefer Microsoft or well-known providers: Favor packages with a strong reputation and broad adoption.

Document your vetting process so every team member follows the same standards.

7.3.2 Implementing Software Composition Analysis (SCA) Tools in the Pipeline

Automated tooling is essential for managing dependencies at scale. SCA tools scan your project for known vulnerabilities in both direct and transitive dependencies.

Popular SCA Tools for .NET:

Integrate these tools into your CI/CD pipeline to catch risky packages before they reach production.

Example: Enabling Dependabot in GitHub

Add a .github/dependabot.yml file to automate updates and alerts for outdated packages:

version: 2
updates:
  - package-ecosystem: "nuget"
    directory: "/"
    schedule:
      interval: "daily"

7.4 Mitigation Checklist for Architects

  • Regularly audit and update all dependencies (direct and transitive)
  • Use automated SCA tools in your build pipeline
  • Vet new libraries for maintenance history and reputation
  • Remove unused packages from your solution
  • Document your component governance strategy and educate your team
  • Monitor security advisories for all critical packages

8 A07:2021 – Identification and Authentication Failures

8.1 Defining the Risk: Confirming User Identity in a Hostile World

Authentication is the front door to your application. If you can’t reliably confirm who’s at the door, nothing else matters—authorization, auditing, and access control all break down.

Identification and authentication failures allow attackers to masquerade as legitimate users. Weaknesses can range from predictable login forms to flaws in password storage, broken session handling, or improper use of authentication tokens.

For .NET applications, where authentication can be complex (especially with APIs, microservices, and hybrid cloud/on-prem scenarios), these risks are ever-present.

8.2 Common Failures in .NET Authentication Schemes

8.2.1 Weak Password Policies and Storage (Password Hashing vs. Encryption)

It’s easy to underestimate the importance of robust password management. Some teams still store passwords using reversible encryption or outdated hash algorithms (like MD5 or SHA1).

Modern Approach: Use salted, adaptive hashing algorithms like PBKDF2 (which ASP.NET Core Identity uses by default), Argon2, or bcrypt. Never store plaintext or simply encrypted passwords.

Code Example: Secure Password Hashing with ASP.NET Core Identity

var passwordHasher = new PasswordHasher<ApplicationUser>();
var hashedPassword = passwordHasher.HashPassword(user, password);

8.2.2 Session Management Flaws: Session Fixation and Improper Invalidation

Session fixation occurs when an attacker sets or steals a session ID before a victim logs in. Improper session invalidation allows sessions to remain active after logout or password change.

Mitigation:

  • Regenerate session tokens after authentication.
  • Invalidate sessions on logout or credential changes.
  • Store tokens securely (not in URLs or client-side storage).

8.2.3 JWT (JSON Web Token) Implementation Pitfalls

JWTs are popular for stateless authentication in APIs. However, poor implementations can introduce vulnerabilities:

  • None algorithm vulnerability: Accepting tokens with alg: none.
  • Unvalidated signatures: Not verifying JWT signatures.
  • Long-lived tokens: Tokens that never expire.
  • Insecure secret storage: Using weak secrets for signing.

Always:

  • Validate token signatures and claims.
  • Use strong, rotating signing keys.
  • Implement token expiration and refresh flows.

8.3 Architecting a Secure Authentication System

8.3.1 Leveraging ASP.NET Core Identity for Secure Credential Management

ASP.NET Core Identity handles many security best practices by default:

  • Uses PBKDF2 for password hashing
  • Handles lockout, two-factor authentication, and password recovery
  • Supports external identity providers (OAuth, OpenID Connect, SAML)

Sample: Configuring Identity in Startup

services.AddDefaultIdentity<ApplicationUser>(options =>
{
    options.Password.RequireDigit = true;
    options.Password.RequiredLength = 12;
    options.Password.RequireNonAlphanumeric = true;
    options.Lockout.MaxFailedAccessAttempts = 5;
})
.AddEntityFrameworkStores<ApplicationDbContext>();

8.3.2 Implementing Multi-Factor Authentication (MFA)

MFA drastically reduces the risk of account takeover. ASP.NET Core Identity supports MFA via email, SMS, or authenticator apps.

Best Practice: Make MFA opt-out for sensitive roles, not opt-in.

8.3.3 Best Practices for Session Management and Token Handling

  • Store JWTs securely (prefer httpOnly cookies over localStorage)
  • Set short expiration for access tokens; use refresh tokens when necessary
  • Always use secure, randomly generated secrets for signing
  • Invalidate tokens on password change or suspicious activity

8.4 Mitigation Checklist for Architects

  • Enforce strong password policies and storage (PBKDF2, Argon2)
  • Implement lockout, brute-force, and MFA protections
  • Regenerate and invalidate sessions/tokens as needed
  • Always validate JWTs and avoid “alg: none”
  • Use short-lived tokens and secure cookie storage
  • Monitor authentication flows for anomalies and log all relevant events

9 A08:2021 – Software and Data Integrity Failures

9.1 Defining the Risk: Trusting Untrusted Data

Software and data integrity failures occur when applications do not protect against unauthorized changes to code, data, or configuration. This risk covers everything from insecure deserialization to unchecked package sources or scripts modified during deployment.

For .NET developers, this means not only protecting your own code, but also verifying the trustworthiness of third-party packages and ensuring the deployment pipeline hasn’t been tampered with.

9.2 Real-World Scenarios

9.2.1 Insecure Deserialization: The Danger of BinaryFormatter and the Rise of System.Text.Json

Deserialization transforms raw data into objects in memory. When deserializing data from untrusted sources (like user uploads or API payloads), attackers can craft payloads that cause code execution or data manipulation.

The BinaryFormatter Danger: .NET’s legacy BinaryFormatter is inherently unsafe for untrusted data and has been deprecated. Exploits allow arbitrary code execution during deserialization.

Safe Modern Alternatives:

  • System.Text.Json (for JSON)
  • XmlSerializer (with caution—disable type resolution)
  • Third-party libraries like protobuf-net (for trusted, binary formats)

Example: Safe JSON Deserialization

var options = new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true
};
var obj = JsonSerializer.Deserialize<MyType>(jsonString, options);

9.2.2 Supply Chain Attacks: Verifying the Integrity of NuGet Packages

Attackers may compromise package sources, publish malicious updates to existing packages, or target your CI/CD pipeline.

Supply Chain Attack Example: In 2021, attackers uploaded malicious packages that mimicked popular libraries by typosquatting (e.g., NewtonSoft.Json with a capital ‘S’). These packages would exfiltrate secrets or install backdoors.

Always verify the source of your dependencies. Use only official package feeds and signed packages when possible.

9.3 Ensuring Integrity from Code to Deployment

9.3.1 Safe Deserialization Patterns and Libraries

  • Never deserialize untrusted data using unsafe formatters
  • Prefer contract-based serializers (JSON, XML with explicit types)
  • Validate schema before deserialization when possible

9.3.2 Code Signing and Artifact Integrity Checks in Azure DevOps/GitHub Actions

Automate integrity checks in your CI/CD:

  • Sign assemblies and NuGet packages before publishing
  • Use checksums or digital signatures to verify artifacts before deployment
  • Enable two-person integrity checks for production releases

Example: Assembly Signing in .NET

<PropertyGroup>
  <SignAssembly>true</SignAssembly>
  <AssemblyOriginatorKeyFile>mykey.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

Artifact Checks with GitHub Actions

- name: Generate checksum
  run: shasum -a 256 myapp.dll > myapp.dll.sha256
- name: Upload checksum
  uses: actions/upload-artifact@v2
  with:
    name: checksum
    path: myapp.dll.sha256

9.4 Mitigation Checklist for Architects

  • Never use BinaryFormatter or similar unsafe serializers for untrusted data
  • Use strong, contract-based serialization and validate input schemas
  • Rely on official, signed package sources and monitor for supply chain alerts
  • Sign your assemblies and deployment artifacts
  • Implement automated integrity and checksum verification in your pipeline
  • Review and lock down your build and deployment environments

10 A09:2021 – Security Logging and Monitoring Failures

10.1 Defining the Risk: Flying Blind During an Attack

Imagine piloting a jet with no instruments. When something goes wrong, you’ll be the last to know. Security logging and monitoring failures create a similar scenario in application security: attacks go undetected, incidents escalate, and you have little context for forensics or remediation.

Logging and monitoring are often relegated to the last step of development, yet they’re essential for both detection and response. Without robust logging, security breaches may linger undetected for weeks or months, only surfacing when customer data appears on the dark web or critical services are disrupted.

For .NET applications, proper logging isn’t just about collecting data. It’s about collecting the right data, storing it securely, and making it actionable.

10.2 What to Log, What Not to Log, and How to Log It

Logging is a double-edged sword. Too little information leaves you blind. Too much creates noise, making real attacks hard to spot. Logging the wrong things—such as passwords or personal information—can become a liability in itself.

10.2.1 Key Events to Monitor: Logins (Success/Failure), Access Control Failures, High-Value Transactions

At a minimum, every .NET web application should capture:

  • Authentication events: Both successes and failures, including IP address and user agent
  • Authorization failures: Attempts to access restricted resources
  • High-value transactions: Changes to sensitive data, role assignments, or payment activities
  • Administrative actions: Account changes, password resets, security setting modifications
  • Error conditions: Exceptions, unhandled errors, and system failures

Each event should include enough context (user, action, timestamp, correlation ID) to trace what happened—without overwhelming the log with unnecessary detail.

10.2.2 Avoiding Sensitive Data in Logs

Never log sensitive data—such as plaintext passwords, cryptographic secrets, or unmasked credit card numbers. Not only does this create compliance issues (GDPR, PCI-DSS), but attackers often seek out logs specifically for this information.

Example: Masking Sensitive Data with Serilog

Log.Information("User {UserId} attempted login from {IPAddress}", userId, ipAddress);
// Never log: Log.Information("User {UserId} used password {Password}", userId, password);

10.2.3 Structured Logging with Serilog and Seq

Traditional log files are difficult to parse and search. Structured logging—capturing logs in a structured format such as JSON—makes it easier to correlate and analyze security events.

Serilog Example:

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .WriteTo.Seq("http://localhost:5341")
    .Enrich.FromLogContext()
    .CreateLogger();

Structured logs can be ingested by platforms like Seq, Elasticsearch, or Azure Monitor, enabling powerful queries and dashboards.

10.3 Building a Monitoring and Alerting Strategy

Logging without monitoring is like recording the black box but never listening to it. Monitoring transforms raw logs into meaningful insights, enabling real-time detection and rapid response.

10.3.1 Integrating .NET Logging with Azure Monitor and Application Insights

Azure Monitor and Application Insights provide deep integration with ASP.NET Core, allowing you to:

  • Automatically collect request, exception, and dependency telemetry
  • Correlate logs with distributed traces across services
  • Set up custom metrics and dashboards

Enabling Application Insights:

builder.Services.AddApplicationInsightsTelemetry();

Once integrated, you can search, visualize, and alert on security-relevant events across your stack.

10.3.2 Creating Actionable Alerts for Suspicious Activity

Raw logs are only helpful if someone—or something—acts on them. Define actionable alerts for:

  • Multiple failed login attempts in a short period (possible brute force)
  • Unusual activity from a single IP or user account
  • Changes to security settings or permissions
  • Unexpected access to sensitive endpoints

Configure automated notifications (email, SMS, Teams, or Slack) and escalation paths for critical events.

10.4 Mitigation Checklist for Architects

  • Define and log all critical security events (auth, access, admin actions)
  • Mask or exclude sensitive data from all logs
  • Adopt structured logging (Serilog, NLog, etc.) for better analysis
  • Integrate with centralized logging and monitoring platforms
  • Create actionable, well-defined alerts for suspicious activity
  • Regularly review and update your logging and monitoring strategy

11 A10:2021 – Server-Side Request Forgery (SSRF)

11.1 Defining the Risk: Making the Server an Unwitting Accomplice

Server-Side Request Forgery (SSRF) vulnerabilities let attackers trick your server into making HTTP requests to unintended destinations—internal networks, cloud metadata endpoints, or even other services behind your firewalls.

When a .NET application fetches URLs or resources based on user-supplied input, it risks being turned into a proxy for malicious activity. SSRF is especially dangerous in cloud environments, where internal APIs and metadata endpoints can expose credentials, configurations, or privileged access.

11.2 How SSRF Manifests in .NET Applications

11.2.1 Code Example: A Vulnerable HttpClient Implementation

Suppose you offer a feature for users to upload images by supplying a URL. The following implementation is vulnerable to SSRF:

[HttpPost]
public async Task<IActionResult> FetchImage([FromBody] ImageRequest req)
{
    var httpClient = new HttpClient();
    var imageBytes = await httpClient.GetByteArrayAsync(req.Url); // UNSAFE!
    // ... further processing
}

Here, the application will fetch any URL provided—including internal resources such as http://localhost/admin or cloud endpoints.

11.2.2 The Cloud Context: SSRF as a Gateway to Internal Azure Services

In Azure, internal services—like the Azure Instance Metadata Service—are accessible via special endpoints. SSRF attacks targeting these endpoints can exfiltrate secrets and escalate privileges.

11.3 SSRF Mitigation Techniques

11.3.1 Input Validation and Allow-Listing Destination URLs

Never trust user input to dictate outbound requests. Apply strict validation:

  • Allow-list only the domains or IPs you expect (e.g., trusted CDN, image service)
  • Disallow local IP ranges (127.0.0.1, 169.254.169.254, etc.)
  • Use DNS and IP parsing to validate resolved endpoints

Example: Allow-Listing URLs

private static readonly List<string> AllowedDomains = new() { "images.example.com" };

bool IsAllowedUrl(string url)
{
    var uri = new Uri(url);
    return AllowedDomains.Contains(uri.Host, StringComparer.OrdinalIgnoreCase);
}

11.3.2 Using Network Controls (e.g., Azure NSGs) to Restrict Outbound Traffic

Application-layer controls are crucial, but defense-in-depth is even better. Restrict your app’s outbound traffic using:

  • Azure Network Security Groups (NSGs)
  • Private endpoints
  • Egress firewall rules

This way, even if code validation fails, your infrastructure blocks suspicious or dangerous requests.

11.4 Mitigation Checklist for Architects

  • Never make outbound requests to user-supplied URLs without validation
  • Allow-list safe domains or IP ranges; deny localhost and cloud metadata endpoints
  • Implement DNS and IP filtering at the application level
  • Use network security controls to limit outbound traffic
  • Regularly review code for features that accept or fetch remote URLs

12 Conclusion: Cultivating a Culture of Security

12.1 Shifting Left: Integrating Security into the SDLC

Modern security isn’t a bolt-on. It’s an integral, continuous process woven through every phase of the Software Development Lifecycle (SDLC). “Shifting left” means bringing security considerations—threat modeling, code reviews, dependency scanning, testing—closer to the start of the project.

For .NET architects, this means:

  • Incorporating security stories and acceptance criteria in sprints
  • Running automated SCA, static analysis, and dependency checks on every pull request
  • Reviewing designs and data flows with security in mind
  • Testing for vulnerabilities before code ever sees production

12.2 The Architect’s Role as a Security Champion

As an architect, you are more than a designer of systems—you are a champion for secure, reliable applications. This role is proactive, not reactive:

  • Advocate for secure-by-default patterns in your architecture
  • Foster collaboration with security, development, and operations teams
  • Educate peers and juniors on both theory and practice (e.g., the OWASP Top 10)
  • Lead by example in code reviews and technical decision-making

Being a security champion isn’t about knowing all the answers. It’s about driving the right questions and embedding security into your team’s culture.

Building secure .NET applications is an ongoing responsibility. Here are a few principles to take with you:

  • Security is never “done.” Treat it as an evolving practice.
  • Automated tooling is your ally—use it wisely, but don’t rely on it alone.
  • Know your stack, know your risks, and never stop learning.
Advertisement