The OWASP Top 10 represents the most critical security risks facing web applications today. This comprehensive guide provides practical, production-ready implementations for all ten OWASP Top 10 2021 categories using ASP.NET Core and .NET 10. Through detailed code examples, architectural patterns, and security best practices, developers will learn how to build secure APIs that protect against common vulnerabilities including broken access control, injection attacks, cryptographic failures, and more. Each category is addressed with real-world scenarios, complete implementations, and testing strategies.
1. Introduction
The Open Web Application Security Project (OWASP) Top 10 is the industry-standard document that provides awareness of the most critical web application security risks. First published in 2003 and updated regularly, the OWASP Top 10 serves as a guide for developers, security professionals, and organizations to understand and mitigate the most prevalent security threats.
Why OWASP Top 10 Matters
- Industry Standard: Recognized globally as the baseline for web application security
- Compliance Requirements: Many security standards reference OWASP Top 10
- Risk-Based Approach: Focuses on the most impactful vulnerabilities
- Practical Guidance: Provides actionable recommendations for developers
This Guide Covers All OWASP Top 10 2021 Categories
This article presents comprehensive security implementations for all ten OWASP Top 10 2021 categories with practical ASP.NET Core examples:
- A01:2021 – Broken Access Control
- A02:2021 – Cryptographic Failures
- A03:2021 – Injection
- A04:2021 – Insecure Design
- A05:2021 – Security Misconfiguration
- A06:2021 – Vulnerable and Outdated Components
- A07:2021 – Identification and Authentication Failures
- A08:2021 – Software and Data Integrity Failures
- A09:2021 – Security Logging and Monitoring Failures
- A10:2021 – Server-Side Request Forgery (SSRF)
2. OWASP Top 10 2021: Complete Overview
Understanding the OWASP Top 10 2021 Changes
The 2021 edition introduced significant updates:
- Three new categories were added
- Four categories were renamed
- Some categories were consolidated based on real-world data
The Complete List with Practical Focus
- A01:2021 – Broken Access Control ⬆️ (moved up from #5)
- Impact: Unauthorized access to resources and data
- Examples: URL manipulation, privilege escalation, CORS misconfiguration
- Focus: Authorization patterns, role-based access, resource-level permissions
- A02:2021 – Cryptographic Failures 🆕 (renamed from Sensitive Data Exposure)
- Impact: Exposure of sensitive data, password theft
- Examples: Weak encryption, plain text storage, insecure protocols
- Focus: Hashing algorithms, encryption at rest and in transit, key management
- A03:2021 – Injection ⬇️ (dropped from #1)
- Impact: Data theft, data loss, denial of service
- Examples: SQL injection, NoSQL injection, command injection
- Focus: Input validation, parameterized queries, ORM usage
- A04:2021 – Insecure Design 🆕 (new category)
- Impact: Wide variety of vulnerabilities due to design flaws
- Examples: Missing threat modeling, lack of security controls
- Focus: Secure design patterns, threat modeling, defense in depth
- A05:2021 – Security Misconfiguration ⬇️ (dropped from #6)
- Impact: System compromise, data breach
- Examples: Default credentials, unnecessary features, missing security headers
- Focus: Configuration management, secure defaults, hardening
- A06:2021 – Vulnerable and Outdated Components ⬇️
- Impact: System takeover, data breach
- Examples: Unpatched libraries, EOL software, unknown dependencies
- Focus: Dependency management, regular updates, vulnerability scanning
- A07:2021 – Identification and Authentication Failures ⬇️ (renamed)
- Impact: Account takeover, identity theft
- Examples: Weak passwords, session fixation, missing MFA
- Focus: Strong authentication, session management, credential protection
- A08:2021 – Software and Data Integrity Failures 🆕 (new category)
- Impact: Unauthorized code execution, data corruption
- Examples: Insecure CI/CD, unsigned updates, serialization vulnerabilities
- Focus: Code signing, integrity verification, secure pipelines
- A09:2021 – Security Logging and Monitoring Failures ⬇️
- Impact: Delayed breach detection, forensic challenges
- Examples: Missing logs, insufficient monitoring, no alerting
- Focus: Comprehensive logging, monitoring, incident response
- A10:2021 – Server-Side Request Forgery (SSRF) 🆕 (new category)
- Impact: Internal system access, data theft
- Examples: Unvalidated URLs, metadata service abuse
- Focus: URL validation, network segmentation, whitelisting
Architecture Overview for This Guide
We'll build a complete ASP.NET Core API implementing security controls for all ten categories:
┌─────────────────────────────────────────────────────────────┐
│ ASP.NET Core API │
│ (.NET 10) │
└───────────────────────────┬─────────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────────┐
│ Security Middleware Pipeline │
├─────────────────────────────────────────────────────────────┤
│ 1. Security Headers Middleware (A05) │
│ 2. Injection Protection Middleware (A03) │
│ 3. Rate Limiting (A05) │
│ 4. CORS Configuration (A01, A05) │
│ 5. JWT Validation Logging (A09) │
│ 6. Authentication (A07) │
│ 7. Authorization (A01) │
│ 8. Custom Validation Middleware (A01, A07) │
└───────────────────────────┬─────────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────────┐
│ Application Layer │
├─────────────────────────────────────────────────────────────┤
│ • Secure Design Patterns (A04) │
│ • Repository Pattern with Access Control (A01, A04) │
│ • Input Validation & Sanitization (A03) │
│ • Audit Trail & Data Integrity (A08, A09) │
│ • Secure External Communication (A10) │
└───────────────────────────┬─────────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────────┐
│ Data Layer │
├─────────────────────────────────────────────────────────────┤
│ • Entity Framework Core (A03) │
│ • Encrypted Fields (A02) │
│ • Audit Interceptor (A08, A09) │
│ • Change Tracking (A08) │
└─────────────────────────────────────────────────────────────┘
3. A01:2021 – Broken Access Control
3.1 Understanding the Threat
Broken Access Control jumped from #5 to #1 in 2021, representing 94% of applications tested having some form of broken access control. This category includes:
- Violation of least privilege: Users have more permissions than needed
- Bypassing access control checks: URL manipulation, force browsing
- Elevation of privilege: Acting as admin without being logged in
- Metadata manipulation: Tampering JWT tokens, cookies
- CORS misconfiguration: Allowing unauthorized domains
3.2 Real-World Attack Scenarios
Scenario 1: Insecure Direct Object Reference (IDOR)
GET /api/users/123/profile
→ Attacker changes to /api/users/456/profile
→ Accesses another user's data
Scenario 2: Function Level Access Control Missing
POST /api/admin/delete-user
→ Regular user can access admin endpoints
→ Deletes other users
3.3 ASP.NET Core Implementation
Role-Based Authorization
// Program.cs - Configure authorization policies
builder.Services.AddAuthorization(options =>
{
// Simple role-based policy
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
// Multiple roles
options.AddPolicy("ModeratorOrAdmin", policy =>
policy.RequireRole("Admin", "Moderator"));
// Claims-based policy
options.AddPolicy("CanManageUsers", policy =>
policy.RequireClaim("Permission", "users.manage"));
// Custom requirement policy
options.AddPolicy("OwnerOrAdmin", policy =>
policy.Requirements.Add(new ResourceOwnerRequirement()));
});
// Custom authorization requirement
public class ResourceOwnerRequirement : IAuthorizationRequirement { }
public class ResourceOwnerHandler : AuthorizationHandler<ResourceOwnerRequirement>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ResourceOwnerHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ResourceOwnerRequirement requirement)
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext == null)
{
context.Fail();
return Task.CompletedTask;
}
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var resourceOwnerId = httpContext.GetRouteValue("userId")?.ToString();
// Check if user is admin OR owns the resource
if (context.User.IsInRole("Admin") || userId == resourceOwnerId)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
// Register handler
builder.Services.AddSingleton<IAuthorizationHandler, ResourceOwnerHandler>();
builder.Services.AddHttpContextAccessor();
Resource-Level Authorization in Endpoints
namespace SecurityApp.API.Endpoints;
public static class UserEndpoints
{
public static IEndpointRouteBuilder MapUserEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/users").WithTags("Users");
// ❌ INSECURE - No authorization check
group.MapGet("/{userId}/profile", async (string userId, ApplicationDbContext db) =>
{
var user = await db.Users.FindAsync(userId);
return Results.Ok(user);
});
// ✅ SECURE - Policy-based authorization
group.MapGet("/{userId}/profile-secure", async (
string userId,
HttpContext httpContext,
ApplicationDbContext db) =>
{
var currentUserId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var isAdmin = httpContext.User.IsInRole("Admin");
// Check authorization
if (currentUserId != userId && !isAdmin)
{
return Results.Forbid();
}
var user = await db.Users.FindAsync(userId);
return user != null ? Results.Ok(user) : Results.NotFound();
})
.RequireAuthorization(); // Requires authentication
// ✅ SECURE - Using custom policy
group.MapPut("/{userId}/update", async (
string userId,
[FromBody] UpdateUserRequest request,
ApplicationDbContext db) =>
{
var user = await db.Users.FindAsync(userId);
if (user == null) return Results.NotFound();
user.FirstName = request.FirstName;
user.LastName = request.LastName;
await db.SaveChangesAsync();
return Results.Ok(user);
})
.RequireAuthorization("OwnerOrAdmin");
// ✅ SECURE - Admin only
group.MapDelete("/{userId}", async (
string userId,
ApplicationDbContext db) =>
{
var user = await db.Users.FindAsync(userId);
if (user == null) return Results.NotFound();
db.Users.Remove(user);
await db.SaveChangesAsync();
return Results.NoContent();
})
.RequireAuthorization("AdminOnly");
return app;
}
}
public record UpdateUserRequest(string FirstName, string LastName);
Repository Pattern with Built-in Authorization
namespace SecurityApp.API.Repositories;
public interface ISecureRepository<T> where T : class
{
Task<T?> GetByIdAsync(string id, string requestingUserId, string[] roles);
Task<IEnumerable<T>> GetAllAsync(string requestingUserId, string[] roles);
Task<bool> UpdateAsync(T entity, string requestingUserId, string[] roles);
Task<bool> DeleteAsync(string id, string requestingUserId, string[] roles);
}
public class UserSecureRepository : ISecureRepository<ApplicationUser>
{
private readonly ApplicationDbContext _context;
private readonly ILogger<UserSecureRepository> _logger;
public UserSecureRepository(
ApplicationDbContext context,
ILogger<UserSecureRepository> logger)
{
_context = context;
_logger = logger;
}
public async Task<ApplicationUser?> GetByIdAsync(
string id,
string requestingUserId,
string[] roles)
{
// Admins can view any user
if (roles.Contains("Admin"))
{
return await _context.Users.FindAsync(id);
}
// Users can only view themselves
if (id != requestingUserId)
{
_logger.LogWarning(
"Access denied: User {RequestingUserId} attempted to view user {TargetUserId}",
requestingUserId, id);
return null;
}
return await _context.Users.FindAsync(id);
}
public async Task<IEnumerable<ApplicationUser>> GetAllAsync(
string requestingUserId,
string[] roles)
{
// Only admins can list all users
if (!roles.Contains("Admin"))
{
_logger.LogWarning(
"Access denied: Non-admin user {UserId} attempted to list all users",
requestingUserId);
return Enumerable.Empty<ApplicationUser>();
}
return await _context.Users.ToListAsync();
}
public async Task<bool> UpdateAsync(
ApplicationUser entity,
string requestingUserId,
string[] roles)
{
// Users can only update themselves, admins can update anyone
if (entity.Id != requestingUserId && !roles.Contains("Admin"))
{
_logger.LogWarning(
"Access denied: User {RequestingUserId} attempted to update user {TargetUserId}",
requestingUserId, entity.Id);
return false;
}
_context.Users.Update(entity);
await _context.SaveChangesAsync();
return true;
}
public async Task<bool> DeleteAsync(
string id,
string requestingUserId,
string[] roles)
{
// Only admins can delete users
if (!roles.Contains("Admin"))
{
_logger.LogWarning(
"Access denied: Non-admin user {UserId} attempted to delete user {TargetUserId}",
requestingUserId, id);
return false;
}
var user = await _context.Users.FindAsync(id);
if (user == null) return false;
_context.Users.Remove(user);
await _context.SaveChangesAsync();
return true;
}
}
CORS Configuration (Preventing Unauthorized Cross-Origin Access)
// Program.cs
builder.Services.AddCors(options =>
{
// ❌ INSECURE - Allows any origin
options.AddPolicy("InsecurePolicy", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
// ✅ SECURE - Specific origins only
options.AddPolicy("SecurePolicy", policy =>
{
policy.WithOrigins(
"https://yourdomain.com",
"https://www.yourdomain.com",
"https://app.yourdomain.com")
.AllowedMethods("GET", "POST", "PUT", "DELETE")
.AllowedHeaders("Authorization", "Content-Type", "Accept")
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromHours(1));
});
// ✅ SECURE - Configuration-based origins
options.AddPolicy("ProductionPolicy", policy =>
{
var allowedOrigins = builder.Configuration
.GetSection("Cors:AllowedOrigins")
.Get<string[]>() ?? Array.Empty<string>();
policy.WithOrigins(allowedOrigins)
.AllowedMethods("GET", "POST", "PUT", "DELETE")
.AllowedHeaders("Authorization", "Content-Type")
.AllowCredentials();
});
});
// In Middleware pipeline
app.UseCors("SecurePolicy");
3.4 Testing Access Control
[Fact]
public async Task GetUserProfile_AsOwner_ShouldSucceed()
{
// Arrange
var userId = "user-123";
var token = GenerateTokenForUser(userId, new[] { "User" });
// Act
var response = await _client.GetAsync($"/api/users/{userId}/profile-secure",
request => request.Headers.Authorization = new("Bearer", token));
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task GetUserProfile_AsOtherUser_ShouldForbid()
{
// Arrange
var userId = "user-123";
var otherUserId = "user-456";
var token = GenerateTokenForUser(otherUserId, new[] { "User" });
// Act
var response = await _client.GetAsync($"/api/users/{userId}/profile-secure",
request => request.Headers.Authorization = new("Bearer", token));
// Assert
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
public async Task GetUserProfile_AsAdmin_ShouldSucceed()
{
// Arrange
var userId = "user-123";
var token = GenerateTokenForUser("admin-789", new[] { "Admin" });
// Act
var response = await _client.GetAsync($"/api/users/{userId}/profile-secure",
request => request.Headers.Authorization = new("Bearer", token));
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task DeleteUser_AsRegularUser_ShouldForbid()
{
// Arrange
var userId = "user-123";
var token = GenerateTokenForUser("user-456", new[] { "User" });
// Act
var response = await _client.DeleteAsync($"/api/users/{userId}",
request => request.Headers.Authorization = new("Bearer", token));
// Assert
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
Key Takeaways for A01:
- ✅ Implement authorization at multiple layers (middleware, endpoint, repository)
- ✅ Use policy-based authorization for complex scenarios
- ✅ Always validate resource ownership
- ✅ Configure CORS with specific origins
- ✅ Log authorization failures for security monitoring
- ✅ Default to deny access, explicitly grant permissions
- ✅ Test all access control scenarios
4. A02:2021 – Cryptographic Failures
4.1 Understanding the Threat
Previously known as "Sensitive Data Exposure," this category focuses on failures related to cryptography (or lack thereof). Common issues include:
- Transmitting data in clear text: HTTP instead of HTTPS
- Using weak cryptographic algorithms: MD5, SHA1 for passwords
- Improper key management: Hardcoded keys, weak keys
- Not encrypting sensitive data: Passwords, PII, credit cards
- Inadequate random number generation: Predictable tokens
4.2 ASP.NET Core Implementation
Password Hashing with Identity
// Program.cs - Configure password hashing
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
// Strong password requirements
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequiredLength = 12; // Minimum 12 characters
options.Password.RequiredUniqueChars = 4;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
// Use PBKDF2 with 100,000 iterations (ASP.NET Core Identity default)
.AddPasswordHasher<ApplicationUser>();
// Custom password hasher if needed
public class CustomPasswordHasher : IPasswordHasher<ApplicationUser>
{
public string HashPassword(ApplicationUser user, string password)
{
// Use Argon2id (winner of Password Hashing Competition)
return Argon2.Hash(password);
}
public PasswordVerificationResult VerifyHashedPassword(
ApplicationUser user, string hashedPassword, string providedPassword)
{
if (Argon2.Verify(hashedPassword, providedPassword))
{
return PasswordVerificationResult.Success;
}
return PasswordVerificationResult.Failed;
}
}
Data Encryption at Rest
using System.Security.Cryptography;
using System.Text;
namespace SecurityApp.API.Services;
public interface IEncryptionService
{
string Encrypt(string plainText);
string Decrypt(string cipherText);
}
public class AesEncryptionService : IEncryptionService
{
private readonly byte[] _key;
private readonly byte[] _iv;
public AesEncryptionService(IConfiguration configuration)
{
// Load from configuration or key vault
var keyString = configuration["Encryption:Key"]
?? throw new InvalidOperationException("Encryption key not configured");
var ivString = configuration["Encryption:IV"]
?? throw new InvalidOperationException("Encryption IV not configured");
_key = Convert.FromBase64String(keyString);
_iv = Convert.FromBase64String(ivString);
// Validate key sizes
if (_key.Length != 32) // 256-bit key
throw new ArgumentException("Key must be 256 bits");
if (_iv.Length != 16) // 128-bit IV
throw new ArgumentException("IV must be 128 bits");
}
public string Encrypt(string plainText)
{
if (string.IsNullOrEmpty(plainText))
throw new ArgumentNullException(nameof(plainText));
using var aes = Aes.Create();
aes.Key = _key;
aes.IV = _iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
var encryptor = aes.CreateEncryptor();
using var ms = new MemoryStream();
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
using (var sw = new StreamWriter(cs))
{
sw.Write(plainText);
}
return Convert.ToBase64String(ms.ToArray());
}
public string Decrypt(string cipherText)
{
if (string.IsNullOrEmpty(cipherText))
throw new ArgumentNullException(nameof(cipherText));
var buffer = Convert.FromBase64String(cipherText);
using var aes = Aes.Create();
aes.Key = _key;
aes.IV = _iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
var decryptor = aes.CreateDecryptor();
using var ms = new MemoryStream(buffer);
using var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read);
using var sr = new StreamReader(cs);
return sr.ReadToEnd();
}
}
// Entity with encrypted field
public class SensitiveData
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string UserId { get; set; } = string.Empty;
// Store encrypted
public string EncryptedSocialSecurity { get; set; } = string.Empty;
public string EncryptedCreditCard { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
// Service layer encryption
public class SensitiveDataService
{
private readonly ApplicationDbContext _context;
private readonly IEncryptionService _encryption;
public SensitiveDataService(
ApplicationDbContext context,
IEncryptionService encryption)
{
_context = context;
_encryption = encryption;
}
public async Task SaveSensitiveDataAsync(
string userId, string ssn, string creditCard)
{
var data = new SensitiveData
{
UserId = userId,
EncryptedSocialSecurity = _encryption.Encrypt(ssn),
EncryptedCreditCard = _encryption.Encrypt(creditCard)
};
_context.SensitiveData.Add(data);
await _context.SaveChangesAsync();
}
public async Task<(string ssn, string creditCard)?> GetSensitiveDataAsync(
string userId)
{
var data = await _context.SensitiveData
.FirstOrDefaultAsync(d => d.UserId == userId);
if (data == null) return null;
return (
_encryption.Decrypt(data.EncryptedSocialSecurity),
_encryption.Decrypt(data.EncryptedCreditCard)
);
}
}
JWT Token Security
// Program.cs
var jwtOptions = builder.Configuration.GetSection("JwtOptions").Get<JwtOptions>()!;
// Validate JWT configuration
if (jwtOptions.SecretKey.Length < 32)
throw new InvalidOperationException("JWT secret key must be at least 256 bits (32 characters)");
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtOptions.Issuer,
ValidAudience = jwtOptions.Audience,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtOptions.SecretKey)),
ClockSkew = TimeSpan.Zero, // No tolerance for expiry
RequireExpirationTime = true,
RequireSignedTokens = true
};
// Enforce HTTPS
options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
// Token validation events
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// Only accept tokens from Authorization header
var token = context.Request.Headers.Authorization
.ToString().Replace("Bearer ", "");
if (!string.IsNullOrEmpty(token))
{
context.Token = token;
}
return Task.CompletedTask;
}
};
});
// JWT Generation with strong algorithms
public class SecureJwtTokenService : IJwtTokenService
{
private readonly JwtOptions _options;
public string GenerateAccessToken(ApplicationUser user, IList<string> roles)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id),
new(ClaimTypes.Email, user.Email!),
new(JwtRegisteredClaimNames.Sub, user.Id),
new(JwtRegisteredClaimNames.Email, user.Email!),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new(JwtRegisteredClaimNames.Iat,
DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())
};
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
// Use HMAC-SHA256 (HS256) - Symmetric algorithm
var securityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_options.SecretKey));
var credentials = new SigningCredentials(
securityKey,
SecurityAlgorithms.HmacSha256); // Strong algorithm
var token = new JwtSecurityToken(
issuer: _options.Issuer,
audience: _options.Audience,
claims: claims,
notBefore: DateTime.UtcNow,
expires: DateTime.UtcNow.AddMinutes(_options.ExpirationMinutes),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public string GenerateRefreshToken()
{
// Use cryptographically secure random number generator
var randomBytes = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomBytes);
return Convert.ToBase64String(randomBytes);
}
}
HTTPS Enforcement
// Program.cs
builder.Services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
options.HttpsPort = 443;
});
builder.Services.AddHsts(options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(365);
});
var app = builder.Build();
// Force HTTPS in production
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();
Key Takeaways for A02:
- ✅ Use strong hashing algorithms (Argon2, PBKDF2) for passwords
- ✅ Encrypt sensitive data at rest using AES-256
- ✅ Use HTTPS everywhere (enforce with HSTS)
- ✅ Generate JWT tokens with strong algorithms (HS256, RS256)
- ✅ Use cryptographically secure random generators
- ✅ Store encryption keys securely (Azure Key Vault, AWS KMS)
- ✅ Never hardcode secrets in source code
- ✅ Implement proper key rotation strategies
4.1 Problem Statement
Authentication failures occur when:
- Session management is improper
- Credentials are not properly protected
- Session IDs are exposed in URLs
- Tokens don't expire or have long expiration times
- Passwords, session IDs, and other credentials are sent over unencrypted connections
4.2 Solution: JWT Token Fingerprinting
Token fingerprinting binds the JWT token to the specific client environment, making stolen tokens useless if used from a different context.
Implementation: HTTP Context Extensions
using System.Security.Cryptography;
using System.Text;
namespace SecurityApp.API.Extensions;
public static class HttpContextExtensions
{
public static string GetClientIpAddress(this HttpContext context)
{
var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
if (!string.IsNullOrEmpty(forwardedFor))
{
return forwardedFor.Split(',')[0].Trim();
}
var realIp = context.Request.Headers["X-Real-IP"].FirstOrDefault();
if (!string.IsNullOrEmpty(realIp))
{
return realIp;
}
return context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
}
public static string GetUserAgent(this HttpContext context)
{
return context.Request.Headers.UserAgent.FirstOrDefault() ?? "unknown";
}
public static string GetClientIpAddressHash(this HttpContext context)
{
var ip = GetClientIpAddress(context);
return ComputeSha256Hash(ip);
}
public static string GetUserAgentHash(this HttpContext context)
{
var userAgent = GetUserAgent(context);
return ComputeSha256Hash(userAgent);
}
private static string ComputeSha256Hash(string input)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
}
Key Security Benefits:
- ✅ Privacy protection: Hashed values prevent exposure of actual IP/User-Agent
- ✅ Deterministic: Same input always produces the same hash for validation
- ✅ Irreversible: Cannot reverse-engineer original values from hash
- ✅ Fixed size: SHA256 produces consistent 64-character hex strings
Enhanced JWT Token Service
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using SecurityApp.API.Models;
using SecurityApp.API.Options;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
namespace SecurityApp.API.Services;
public interface IJwtTokenService
{
string GenerateAccessToken(ApplicationUser user, IList<string> roles,
string clientIpHash, string userAgentHash);
string GenerateRefreshToken();
ClaimsPrincipal? GetPrincipalFromExpiredToken(string token);
}
public class JwtTokenService : IJwtTokenService
{
private readonly JwtOptions _jwtOptions;
public JwtTokenService(IOptions<JwtOptions> jwtOptions)
{
_jwtOptions = jwtOptions.Value;
}
public string GenerateAccessToken(ApplicationUser user, IList<string> roles,
string clientIpHash, string userAgentHash)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id),
new(ClaimTypes.Name, user.UserName!),
new(ClaimTypes.Email, user.Email!),
new(JwtRegisteredClaimNames.Sub, user.Id),
new(JwtRegisteredClaimNames.Email, user.Email!),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new("client_ip_hash", clientIpHash),
new("user_agent_hash", userAgentHash)
};
if (!string.IsNullOrWhiteSpace(user.FirstName))
{
claims.Add(new Claim(ClaimTypes.GivenName, user.FirstName));
}
if (!string.IsNullOrWhiteSpace(user.LastName))
{
claims.Add(new Claim(ClaimTypes.Surname, user.LastName));
}
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var securityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_jwtOptions.SecretKey));
var credentials = new SigningCredentials(
securityKey, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _jwtOptions.Issuer,
audience: _jwtOptions.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_jwtOptions.ExpirationMinutes),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public string GenerateRefreshToken()
{
var randomBytes = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomBytes);
return Convert.ToBase64String(randomBytes);
}
public ClaimsPrincipal? GetPrincipalFromExpiredToken(string token)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_jwtOptions.SecretKey)),
ValidateLifetime = false
};
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(token,
tokenValidationParameters, out var securityToken);
if (securityToken is not JwtSecurityToken jwtSecurityToken ||
!jwtSecurityToken.Header.Alg.Equals(
SecurityAlgorithms.HmacSha256,
StringComparison.InvariantCultureIgnoreCase))
{
throw new SecurityTokenException("Invalid token");
}
return principal;
}
}
4.3 Strong Password Policies
// In Program.cs
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequiredLength = 8;
// Lockout settings - Protection against brute force
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// User settings
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
Security Features:
- ✅ Enforces complex password requirements
- ✅ Implements account lockout after failed attempts
- ✅ Prevents brute force attacks
- ✅ Ensures email uniqueness
5. Addressing OWASP A01:2021 – Broken Access Control
5.1 Problem Statement
Broken access control allows unauthorized users to:
- Access functionality or data they shouldn't
- Modify or delete data
- Perform actions outside their permissions
- Bypass access control checks
5.2 Solution: IP and User-Agent Validation Middleware
IP Validation Middleware
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
namespace SecurityApp.API.Middleware;
public class IpValidationMiddleware
{
private readonly RequestDelegate _next;
public IpValidationMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.User.Identity?.IsAuthenticated == true)
{
var tokenIpHashClaim = context.User.FindFirst("client_ip_hash")?.Value;
if (!string.IsNullOrEmpty(tokenIpHashClaim))
{
var currentIp = GetClientIpAddress(context);
var currentIpHash = ComputeSha256Hash(currentIp);
if (tokenIpHashClaim != currentIpHash)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsJsonAsync(new
{
Message = "IP address mismatch. Token was issued for a different IP address."
});
return;
}
}
}
await _next(context);
}
private static string GetClientIpAddress(HttpContext context)
{
var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
if (!string.IsNullOrEmpty(forwardedFor))
{
return forwardedFor.Split(',')[0].Trim();
}
var realIp = context.Request.Headers["X-Real-IP"].FirstOrDefault();
if (!string.IsNullOrEmpty(realIp))
{
return realIp;
}
return context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
}
private static string ComputeSha256Hash(string input)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
}
User-Agent Validation Middleware
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
namespace SecurityApp.API.Middleware;
public class UserAgentValidationMiddleware
{
private readonly RequestDelegate _next;
public UserAgentValidationMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.User.Identity?.IsAuthenticated == true)
{
var tokenUserAgentHashClaim = context.User.FindFirst("user_agent_hash")?.Value;
if (!string.IsNullOrEmpty(tokenUserAgentHashClaim))
{
var currentUserAgent = GetUserAgent(context);
var currentUserAgentHash = ComputeSha256Hash(currentUserAgent);
if (tokenUserAgentHashClaim != currentUserAgentHash)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsJsonAsync(new
{
Message = "User-Agent mismatch. Token was issued for a different browser or device."
});
return;
}
}
}
await _next(context);
}
private static string GetUserAgent(HttpContext context)
{
return context.Request.Headers.UserAgent.FirstOrDefault() ?? "unknown";
}
private static string ComputeSha256Hash(string input)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
}
How It Prevents Broken Access Control:
- ✅ Binds tokens to specific client environments
- ✅ Prevents token theft and replay attacks
- ✅ Validates every authenticated request
- ✅ Immediate rejection of mismatched requests
5.3 Attack Scenario Prevention
Scenario: Stolen Token Attack
- Without Fingerprinting:
- Attacker steals JWT token (XSS, MITM, etc.)
- Attacker uses token from their device
- ✗ Attacker gains full access
- With Fingerprinting:
- Attacker steals JWT token
- Attacker uses token from their device
- Middleware detects IP/User-Agent mismatch
- ✓ Request rejected with 401 Unauthorized
6. Addressing OWASP A02:2021 – Cryptographic Failures
6.1 Problem Statement
Cryptographic failures include:
- Not using encryption for sensitive data
- Using weak or outdated cryptographic algorithms
- Improper key management
- Not enforcing encryption in transit
6.2 Solution: SHA256 Hashing and Strong JWT Configuration
Why SHA256 for Fingerprinting?
private static string ComputeSha256Hash(string input)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
Benefits:
- ✅ One-way function: Cannot reverse to get original value
- ✅ Collision resistant: Extremely unlikely two inputs produce same hash
- ✅ Deterministic: Same input always produces same output
- ✅ Fast: Efficient computation
- ✅ Fixed size: Always 256 bits (64 hex characters)
JWT Security Configuration
// In Program.cs
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtOptions.Issuer,
ValidAudience = jwtOptions.Audience,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtOptions.SecretKey)),
ClockSkew = TimeSpan.Zero // No clock skew tolerance
};
// ... event handlers (shown in next section)
});
Security Features:
- ✅ Validates issuer and audience
- ✅ Enforces token expiration strictly (ClockSkew = 0)
- ✅ Validates signing key
- ✅ Uses HMAC-SHA256 for token signing
6.3 Secure Token Storage
Best Practices Implemented:
- Access tokens: Short-lived (60 minutes)
- Refresh tokens: Longer-lived (7 days), stored in database
- No sensitive data in JWT payload
- Hashed fingerprints instead of raw values
7. Addressing OWASP A09:2021 – Security Logging and Monitoring Failures
7.1 Problem Statement
Insufficient logging and monitoring leads to:
- Inability to detect breaches in time
- No audit trail for investigations
- Missing critical security events
- Lack of alerting mechanisms
7.2 Solution: Comprehensive JWT Validation Logging
JWT Validation Details Middleware
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
namespace SecurityApp.API.Middleware;
public class JwtValidationDetailsMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<JwtValidationDetailsMiddleware> _logger;
public JwtValidationDetailsMiddleware(RequestDelegate next,
ILogger<JwtValidationDetailsMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var authHeader = context.Request.Headers.Authorization.FirstOrDefault();
if (!string.IsNullOrEmpty(authHeader) &&
authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
var token = authHeader["Bearer ".Length..].Trim();
try
{
var handler = new JwtSecurityTokenHandler();
var jwtToken = handler.ReadJwtToken(token);
var now = DateTime.UtcNow;
var validFrom = jwtToken.ValidFrom;
var validTo = jwtToken.ValidTo;
_logger.LogInformation(
"JWT Token Details - Issuer: {Issuer}, Audience: {Audience}, " +
"ValidFrom: {ValidFrom}, ValidTo: {ValidTo}, Now: {Now}, " +
"IsExpired: {IsExpired}",
jwtToken.Issuer,
string.Join(", ", jwtToken.Audiences),
validFrom,
validTo,
now,
now > validTo);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to read JWT token details");
}
}
await _next(context);
}
}
Detailed JWT Bearer Events
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILogger<Program>>();
if (context.Exception is SecurityTokenExpiredException expiredException)
{
logger.LogWarning("Token expired at {ExpiredTime}",
expiredException.Expires);
context.Response.Headers.Append("Token-Expired", "true");
}
else if (context.Exception is SecurityTokenInvalidIssuerException)
{
logger.LogWarning("Invalid Issuer: {Message}",
context.Exception.Message);
context.Response.Headers.Append("Token-Error", "Invalid Issuer");
}
else if (context.Exception is SecurityTokenInvalidAudienceException)
{
logger.LogWarning("Invalid Audience: {Message}",
context.Exception.Message);
context.Response.Headers.Append("Token-Error", "Invalid Audience");
}
else if (context.Exception is SecurityTokenInvalidSignatureException)
{
logger.LogWarning("Invalid Signature: {Message}",
context.Exception.Message);
context.Response.Headers.Append("Token-Error", "Invalid Signature");
}
else
{
logger.LogError(context.Exception,
"Authentication failed: {Message}",
context.Exception.Message);
context.Response.Headers.Append("Token-Error",
context.Exception.GetType().Name);
}
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILogger<Program>>();
var userEmail = context.Principal?.Identity?.Name;
logger.LogInformation(
"Token validated successfully for user: {UserEmail}", userEmail);
return Task.CompletedTask;
},
OnChallenge = context =>
{
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILogger<Program>>();
logger.LogWarning(
"Authentication challenge. Error: {Error}, " +
"ErrorDescription: {ErrorDescription}",
context.Error, context.ErrorDescription);
return Task.CompletedTask;
}
};
7.3 Debug Endpoint for Token Validation
private static IResult ValidateToken(HttpContext httpContext)
{
var authHeader = httpContext.Request.Headers.Authorization.FirstOrDefault();
if (string.IsNullOrEmpty(authHeader) ||
!authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
return Results.BadRequest(new { Message = "No bearer token provided" });
}
var token = authHeader["Bearer ".Length..].Trim();
var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
try
{
var jwtToken = handler.ReadJwtToken(token);
var now = DateTime.UtcNow;
return Results.Ok(new
{
Issuer = jwtToken.Issuer,
Audiences = jwtToken.Audiences,
ValidFrom = jwtToken.ValidFrom,
ValidTo = jwtToken.ValidTo,
CurrentTime = now,
IsExpired = now > jwtToken.ValidTo,
TimeUntilExpiry = jwtToken.ValidTo - now,
Claims = jwtToken.Claims.Select(c => new { c.Type, c.Value })
});
}
catch (Exception ex)
{
return Results.BadRequest(new
{
Message = "Invalid token format",
Error = ex.Message
});
}
}
Logging Benefits:
- ✅ Real-time token validation monitoring
- ✅ Detailed failure reasons (expiry, issuer, audience, signature)
- ✅ Audit trail for security investigations
- ✅ Debug endpoint for troubleshooting
- ✅ Custom response headers for client-side handling
8. Addressing OWASP A03:2021 – Injection
8.1 Problem Statement
Injection flaws occur when untrusted data is sent to an interpreter as part of a command or query. Common types include:
- SQL Injection
- NoSQL Injection
- OS Command Injection
- LDAP Injection
8.2 Solution: Parameterized Queries and Input Validation
SQL Injection Prevention with Entity Framework
// ❌ VULNERABLE CODE - Never do this!
public async Task<ApplicationUser?> GetUserByEmailVulnerable(string email)
{
// Direct string concatenation - SQL Injection risk!
var query = $"SELECT * FROM Users WHERE Email = '{email}'";
return await _context.Users.FromSqlRaw(query).FirstOrDefaultAsync();
}
// ✅ SECURE CODE - Use parameterized queries
public async Task<ApplicationUser?> GetUserByEmailSecure(string email)
{
// Entity Framework automatically parameterizes
return await _context.Users
.FirstOrDefaultAsync(u => u.Email == email);
}
// ✅ SECURE CODE - Explicit parameterization if needed
public async Task<ApplicationUser?> GetUserByEmailWithParameter(string email)
{
return await _context.Users
.FromSqlRaw("SELECT * FROM Users WHERE Email = {0}", email)
.FirstOrDefaultAsync();
}
Input Validation Attribute
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
namespace SecurityApp.API.Validation;
public class SafeStringAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (value is string input)
{
// Prevent common injection patterns
var dangerousPatterns = new[]
{
@"<script",
@"javascript:",
@"onerror=",
@"onload=",
@"eval\(",
@"exec\(",
@"';\s*DROP",
@"--",
@"/*",
@"xp_"
};
foreach (var pattern in dangerousPatterns)
{
if (Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase))
{
return new ValidationResult($"Input contains potentially dangerous content.");
}
}
}
return ValidationResult.Success;
}
}
// Usage in DTOs
public class RegisterRequest
{
[Required]
[EmailAddress]
[SafeString]
public string Email { get; set; } = string.Empty;
[Required]
[SafeString]
[StringLength(100, MinimumLength = 2)]
public string FirstName { get; set; } = string.Empty;
[Required]
[SafeString]
[StringLength(100, MinimumLength = 2)]
public string LastName { get; set; } = string.Empty;
[Required]
[StringLength(100, MinimumLength = 8)]
public string Password { get; set; } = string.Empty;
}
Request Validation Middleware
using System.Text.RegularExpressions;
namespace SecurityApp.API.Middleware;
public class InjectionProtectionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<InjectionProtectionMiddleware> _logger;
private static readonly Regex[] DangerousPatterns = new[]
{
new Regex(@"(\bOR\b|\bAND\b).*=.*", RegexOptions.IgnoreCase),
new Regex(@";\s*DROP\s+TABLE", RegexOptions.IgnoreCase),
new Regex(@"<script[^>]*>.*?</script>", RegexOptions.IgnoreCase | RegexOptions.Singleline),
new Regex(@"javascript\s*:", RegexOptions.IgnoreCase),
new Regex(@"\bexec\s*\(", RegexOptions.IgnoreCase),
new Regex(@"union\s+select", RegexOptions.IgnoreCase)
};
public InjectionProtectionMiddleware(RequestDelegate next,
ILogger<InjectionProtectionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var queryString = context.Request.QueryString.Value;
if (!string.IsNullOrEmpty(queryString))
{
foreach (var pattern in DangerousPatterns)
{
if (pattern.IsMatch(queryString))
{
_logger.LogWarning(
"Potential injection attempt detected. IP: {IP}, Query: {Query}",
context.Connection.RemoteIpAddress,
queryString);
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(new
{
Message = "Invalid request detected."
});
return;
}
}
}
await _next(context);
}
}
Security Benefits:
- ✅ Parameterized queries prevent SQL injection
- ✅ Input validation catches malicious patterns
- ✅ Middleware provides defense in depth
- ✅ Logging helps identify attack attempts
9. Addressing OWASP A04:2021 – Insecure Design
9.1 Problem Statement
Insecure design represents missing or ineffective security controls in the design phase:
- Lack of security requirements
- No threat modeling
- Missing security controls
- Inadequate separation of concerns
9.2 Solution: Secure Design Patterns
Repository Pattern with Security Controls
namespace SecurityApp.API.Repositories;
public interface IUserRepository
{
Task<ApplicationUser?> GetByIdAsync(string userId, string requestingUserId);
Task<ApplicationUser?> GetByEmailAsync(string email);
Task<IEnumerable<ApplicationUser>> GetAllAsync(string requestingUserId, string[] roles);
Task<bool> UpdateAsync(ApplicationUser user, string requestingUserId);
}
public class UserRepository : IUserRepository
{
private readonly ApplicationDbContext _context;
private readonly ILogger<UserRepository> _logger;
public UserRepository(ApplicationDbContext context, ILogger<UserRepository> logger)
{
_context = context;
_logger = logger;
}
// Secure by design - always requires requesting user context
public async Task<ApplicationUser?> GetByIdAsync(string userId, string requestingUserId)
{
// Authorization check at data layer
if (userId != requestingUserId)
{
_logger.LogWarning(
"User {RequestingUser} attempted to access user {TargetUser}",
requestingUserId, userId);
return null;
}
return await _context.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == userId);
}
public async Task<ApplicationUser?> GetByEmailAsync(string email)
{
return await _context.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Email == email);
}
// Only admins can list all users
public async Task<IEnumerable<ApplicationUser>> GetAllAsync(
string requestingUserId, string[] roles)
{
if (!roles.Contains("Admin"))
{
_logger.LogWarning(
"Non-admin user {UserId} attempted to list all users",
requestingUserId);
return Enumerable.Empty<ApplicationUser>();
}
return await _context.Users
.AsNoTracking()
.ToListAsync();
}
// Users can only update their own data
public async Task<bool> UpdateAsync(ApplicationUser user, string requestingUserId)
{
if (user.Id != requestingUserId)
{
_logger.LogWarning(
"User {RequestingUser} attempted to update user {TargetUser}",
requestingUserId, user.Id);
return false;
}
_context.Users.Update(user);
await _context.SaveChangesAsync();
return true;
}
}
Domain-Driven Security Model
namespace SecurityApp.API.Models;
public class SecureDocument
{
public string Id { get; private set; } = Guid.NewGuid().ToString();
public string OwnerId { get; private set; } = string.Empty;
public string Title { get; private set; } = string.Empty;
public string Content { get; private set; } = string.Empty;
public DocumentAccessLevel AccessLevel { get; private set; }
public DateTime CreatedAt { get; private set; }
public DateTime? ModifiedAt { get; private set; }
public bool IsDeleted { get; private set; }
private SecureDocument() { } // EF Core
// Factory method with built-in security
public static SecureDocument Create(string ownerId, string title,
string content, DocumentAccessLevel accessLevel)
{
if (string.IsNullOrWhiteSpace(ownerId))
throw new ArgumentException("Owner ID is required");
if (string.IsNullOrWhiteSpace(title))
throw new ArgumentException("Title is required");
return new SecureDocument
{
OwnerId = ownerId,
Title = title,
Content = content,
AccessLevel = accessLevel,
CreatedAt = DateTime.UtcNow
};
}
// Secure update - checks ownership
public Result Update(string requestingUserId, string newTitle, string newContent)
{
if (requestingUserId != OwnerId)
return Result.Failure("Unauthorized: Only the owner can update this document");
if (IsDeleted)
return Result.Failure("Cannot update deleted document");
Title = newTitle;
Content = newContent;
ModifiedAt = DateTime.UtcNow;
return Result.Success();
}
// Secure delete - soft delete with ownership check
public Result Delete(string requestingUserId)
{
if (requestingUserId != OwnerId)
return Result.Failure("Unauthorized: Only the owner can delete this document");
IsDeleted = true;
ModifiedAt = DateTime.UtcNow;
return Result.Success();
}
// Access control check
public bool CanAccess(string userId, string[] userRoles)
{
if (IsDeleted)
return false;
return AccessLevel switch
{
DocumentAccessLevel.Private => userId == OwnerId,
DocumentAccessLevel.Internal => userRoles.Contains("Employee"),
DocumentAccessLevel.Public => true,
_ => false
};
}
}
public enum DocumentAccessLevel
{
Private = 0,
Internal = 1,
Public = 2
}
public class Result
{
public bool IsSuccess { get; private set; }
public string Error { get; private set; } = string.Empty;
public static Result Success() => new() { IsSuccess = true };
public static Result Failure(string error) => new() { IsSuccess = false, Error = error };
}
Secure Design Principles:
- ✅ Security built into domain models
- ✅ Authorization checks at multiple layers
- ✅ Immutable properties prevent tampering
- ✅ Factory methods ensure valid object creation
- ✅ Explicit access control methods
10. Addressing OWASP A05:2021 – Security Misconfiguration
10.1 Problem Statement
Security misconfiguration includes:
- Missing security headers
- Verbose error messages in production
- Default accounts enabled
- Unnecessary features enabled
- Outdated software
10.2 Solution: Security Configuration Best Practices
Security Headers Middleware
namespace SecurityApp.API.Middleware;
public class SecurityHeadersMiddleware
{
private readonly RequestDelegate _next;
public SecurityHeadersMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Remove server information disclosure
context.Response.Headers.Remove("Server");
context.Response.Headers.Remove("X-Powered-By");
// Prevent clickjacking
context.Response.Headers["X-Frame-Options"] = "DENY";
// Enable XSS protection
context.Response.Headers["X-XSS-Protection"] = "1; mode=block";
// Prevent MIME type sniffing
context.Response.Headers["X-Content-Type-Options"] = "nosniff";
// Content Security Policy
context.Response.Headers["Content-Security-Policy"] =
"default-src 'self'; " +
"script-src 'self'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"frame-ancestors 'none';";
// Referrer Policy
context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
// Permissions Policy
context.Response.Headers["Permissions-Policy"] =
"geolocation=(), microphone=(), camera=()";
// HSTS - Only in production
if (!context.Request.IsHttps)
{
context.Response.Redirect($"https://{context.Request.Host}{context.Request.Path}");
return;
}
context.Response.Headers["Strict-Transport-Security"] =
"max-age=31536000; includeSubDomains; preload";
await _next(context);
}
}
Secure Configuration in Program.cs
var builder = WebApplication.CreateBuilder(args);
// Configure CORS securely
builder.Services.AddCors(options =>
{
options.AddPolicy("SecurePolicy", policy =>
{
policy.WithOrigins(
builder.Configuration.GetSection("AllowedOrigins").Get<string[]>()
?? Array.Empty<string>())
.AllowedMethods("GET", "POST", "PUT", "DELETE")
.AllowedHeaders("Authorization", "Content-Type")
.AllowCredentials()
.SetIsOriginAllowedToAllowWildcardSubdomains();
});
});
// Disable detailed errors in production
if (!builder.Environment.IsDevelopment())
{
builder.Services.AddExceptionHandler(options =>
{
options.ExceptionHandler = async context =>
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(new
{
Message = "An error occurred. Please contact support."
// No stack trace or detailed error info!
});
};
});
}
// Configure HTTPS redirection
builder.Services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
options.HttpsPort = 443;
});
// Rate limiting
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: partition => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1),
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 0
}));
});
var app = builder.Build();
// Security middleware order
app.UseSecurityHeaders(); // Custom middleware
app.UseRateLimiter();
app.UseHttpsRedirection();
app.UseCors("SecurePolicy");
// Only enable Swagger in Development
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
else
{
app.UseExceptionHandler();
}
app.Run();
appsettings.json Configuration
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedOrigins": [
"https://yourdomain.com",
"https://www.yourdomain.com"
],
"JwtOptions": {
"SecretKey": "YOUR-STRONG-SECRET-KEY-MIN-32-CHARACTERS",
"Issuer": "https://api.yourdomain.com",
"Audience": "https://yourdomain.com",
"ExpirationMinutes": 60
},
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=SecurityAppDb;Trusted_Connection=true;TrustServerCertificate=true"
}
}
Configuration Best Practices:
- ✅ Security headers prevent common attacks
- ✅ HTTPS enforcement with HSTS
- ✅ CORS configured with specific origins
- ✅ Rate limiting prevents abuse
- ✅ Detailed errors disabled in production
- ✅ Sensitive configuration externalized
11. Addressing OWASP A06:2021 – Vulnerable and Outdated Components
11.1 Problem Statement
Using components with known vulnerabilities:
- Outdated libraries and frameworks
- Unsupported or unpatched dependencies
- Not tracking dependency vulnerabilities
11.2 Solution: Dependency Management and Scanning
NuGet Package Audit Configuration
<!-- SecurityApp.API.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>14.0</LangVersion>
<!-- Enable NuGet package vulnerability auditing -->
<NuGetAudit>true</NuGetAudit>
<NuGetAuditMode>all</NuGetAuditMode>
<NuGetAuditLevel>low</NuGetAuditLevel>
</PropertyGroup>
<ItemGroup>
<!-- Keep packages up to date -->
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- Security analyzers -->
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
Dependency Health Check Endpoint
namespace SecurityApp.API.Endpoints;
public static class HealthEndpoints
{
public static IEndpointRouteBuilder MapHealthEndpoints(this IEndpointRouteBuilder app)
{
app.MapGet("/health", () => Results.Ok(new
{
Status = "Healthy",
Timestamp = DateTime.UtcNow,
Version = typeof(Program).Assembly.GetName().Version?.ToString()
}))
.WithName("HealthCheck")
.AllowAnonymous();
app.MapGet("/health/dependencies", (IConfiguration config) =>
{
var dependencies = new
{
AspNetCore = typeof(Microsoft.AspNetCore.Builder.WebApplication)
.Assembly.GetName().Version?.ToString(),
EntityFrameworkCore = typeof(Microsoft.EntityFrameworkCore.DbContext)
.Assembly.GetName().Version?.ToString(),
AspNetCoreIdentity = typeof(Microsoft.AspNetCore.Identity.IdentityUser)
.Assembly.GetName().Version?.ToString(),
TargetFramework = "net10.0",
LastChecked = DateTime.UtcNow
};
return Results.Ok(dependencies);
})
.WithName("DependencyCheck")
.RequireAuthorization(policy => policy.RequireRole("Admin"));
return app;
}
}
GitHub Actions CI/CD with Security Scanning
# .github/workflows/security-scan.yml
name: Security Scan
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 0 * * 0' # Weekly scan
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Check for vulnerable packages
run: dotnet list package --vulnerable --include-transitive
- name: Check for outdated packages
run: dotnet list package --outdated
- name: Run security audit
run: dotnet build --configuration Release /p:NuGetAudit=true
- name: Run OWASP Dependency Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'SecurityApp'
path: '.'
format: 'HTML'
Dependency Management Benefits:
- ✅ Automated vulnerability scanning
- ✅ NuGet audit during build
- ✅ Regular dependency updates
- ✅ CI/CD integration
- ✅ Health check endpoints
12. Addressing OWASP A08:2021 – Software and Data Integrity Failures
12.1 Problem Statement
Integrity failures include:
- Insecure CI/CD pipelines
- Auto-update without verification
- Serialization/deserialization vulnerabilities
- Unsigned or unverified code
12.2 Solution: Integrity Verification
Secure Data Transfer Objects with Validation
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace SecurityApp.API.Models;
public class SignedRequest<T> where T : class
{
public T Data { get; set; } = default!;
public string Signature { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
public bool VerifySignature(string secretKey)
{
// Prevent replay attacks - 5 minute window
if (Math.Abs((DateTime.UtcNow - Timestamp).TotalMinutes) > 5)
return false;
var dataJson = JsonSerializer.Serialize(Data);
var computedSignature = ComputeSignature(dataJson, Timestamp, secretKey);
return Signature == computedSignature;
}
public static SignedRequest<T> Create(T data, string secretKey)
{
var timestamp = DateTime.UtcNow;
var dataJson = JsonSerializer.Serialize(data);
var signature = ComputeSignature(dataJson, timestamp, secretKey);
return new SignedRequest<T>
{
Data = data,
Signature = signature,
Timestamp = timestamp
};
}
private static string ComputeSignature(string data, DateTime timestamp, string secretKey)
{
var message = $"{data}|{timestamp:O}";
var keyBytes = Encoding.UTF8.GetBytes(secretKey);
var messageBytes = Encoding.UTF8.GetBytes(message);
var hash = HMACSHA256.HashData(keyBytes, messageBytes);
return Convert.ToBase64String(hash);
}
}
Audit Trail for Data Changes
namespace SecurityApp.API.Models;
public class AuditLog
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string EntityName { get; set; } = string.Empty;
public string EntityId { get; set; } = string.Empty;
public string Action { get; set; } = string.Empty; // Create, Update, Delete
public string? OldValues { get; set; }
public string? NewValues { get; set; }
public string UserId { get; set; } = string.Empty;
public string UserEmail { get; set; } = string.Empty;
public string IpAddress { get; set; } = string.Empty;
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
// Audit interceptor for Entity Framework
public class AuditInterceptor : SaveChangesInterceptor
{
private readonly IHttpContextAccessor _httpContextAccessor;
public AuditInterceptor(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
if (eventData.Context is ApplicationDbContext context)
{
AuditChanges(context);
}
return base.SavingChanges(eventData, result);
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
if (eventData.Context is ApplicationDbContext context)
{
AuditChanges(context);
}
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
private void AuditChanges(ApplicationDbContext context)
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext == null) return;
var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "System";
var userEmail = httpContext.User.FindFirst(ClaimTypes.Email)?.Value ?? "Unknown";
var ipAddress = httpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown";
var entries = context.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added ||
e.State == EntityState.Modified ||
e.State == EntityState.Deleted);
foreach (var entry in entries)
{
var auditLog = new AuditLog
{
EntityName = entry.Entity.GetType().Name,
EntityId = entry.Properties.FirstOrDefault(p => p.Metadata.IsPrimaryKey())?.CurrentValue?.ToString() ?? "Unknown",
Action = entry.State.ToString(),
UserId = userId,
UserEmail = userEmail,
IpAddress = ipAddress,
Timestamp = DateTime.UtcNow
};
if (entry.State == EntityState.Modified)
{
var oldValues = entry.Properties
.Where(p => p.IsModified)
.ToDictionary(p => p.Metadata.Name, p => p.OriginalValue);
var newValues = entry.Properties
.Where(p => p.IsModified)
.ToDictionary(p => p.Metadata.Name, p => p.CurrentValue);
auditLog.OldValues = JsonSerializer.Serialize(oldValues);
auditLog.NewValues = JsonSerializer.Serialize(newValues);
}
else if (entry.State == EntityState.Added)
{
var newValues = entry.Properties
.ToDictionary(p => p.Metadata.Name, p => p.CurrentValue);
auditLog.NewValues = JsonSerializer.Serialize(newValues);
}
else if (entry.State == EntityState.Deleted)
{
var oldValues = entry.Properties
.ToDictionary(p => p.Metadata.Name, p => p.OriginalValue);
auditLog.OldValues = JsonSerializer.Serialize(oldValues);
}
context.Set<AuditLog>().Add(auditLog);
}
}
}
// Register in Program.cs
builder.Services.AddDbContext<ApplicationDbContext>((serviceProvider, options) =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
options.AddInterceptors(serviceProvider.GetRequiredService<AuditInterceptor>());
});
builder.Services.AddScoped<AuditInterceptor>();
builder.Services.AddHttpContextAccessor();
Data Integrity Benefits:
- ✅ HMAC signatures prevent tampering
- ✅ Timestamp validation prevents replay attacks
- ✅ Complete audit trail for all changes
- ✅ Automatic change tracking
- ✅ Forensic analysis capability
13. Addressing OWASP A10:2021 – Server-Side Request Forgery (SSRF)
13.1 Problem Statement
SSRF flaws occur when a web application fetches a remote resource without validating the user-supplied URL:
- Access to internal systems
- Port scanning
- Reading local files
- Bypassing firewalls
13.2 Solution: URL Validation and Whitelist
Secure HTTP Client Service
using System.Net;
namespace SecurityApp.API.Services;
public interface ISecureHttpClient
{
Task<HttpResponseMessage> GetAsync(string url);
Task<HttpResponseMessage> PostAsync(string url, HttpContent content);
}
public class SecureHttpClient : ISecureHttpClient
{
private readonly HttpClient _httpClient;
private readonly ILogger<SecureHttpClient> _logger;
private readonly IConfiguration _configuration;
// Blacklist for internal/private networks
private static readonly IPAddress[] BlockedNetworks = new[]
{
IPAddress.Parse("127.0.0.1"), // Localhost
IPAddress.Parse("0.0.0.0"), // Current network
IPAddress.Parse("10.0.0.0"), // Private network
IPAddress.Parse("172.16.0.0"), // Private network
IPAddress.Parse("192.168.0.0"), // Private network
IPAddress.Parse("169.254.0.0"), // Link-local
IPAddress.Parse("224.0.0.0") // Multicast
};
public SecureHttpClient(HttpClient httpClient, ILogger<SecureHttpClient> logger, IConfiguration configuration)
{
_httpClient = httpClient;
_logger = logger;
_configuration = configuration;
// Set timeout
_httpClient.Timeout = TimeSpan.FromSeconds(30);
}
public async Task<HttpResponseMessage> GetAsync(string url)
{
await ValidateUrlAsync(url);
return await _httpClient.GetAsync(url);
}
public async Task<HttpResponseMessage> PostAsync(string url, HttpContent content)
{
await ValidateUrlAsync(url);
return await _httpClient.PostAsync(url, content);
}
private async Task ValidateUrlAsync(string url)
{
// 1. Check if URL is well-formed
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
_logger.LogWarning("Invalid URL format: {Url}", url);
throw new SecurityException("Invalid URL format");
}
// 2. Only allow HTTPS
if (uri.Scheme != Uri.UriSchemeHttps)
{
_logger.LogWarning("Non-HTTPS URL rejected: {Url}", url);
throw new SecurityException("Only HTTPS URLs are allowed");
}
// 3. Check against whitelist
var allowedDomains = _configuration.GetSection("AllowedExternalDomains").Get<string[]>()
?? Array.Empty<string>();
if (!allowedDomains.Any(domain => uri.Host.EndsWith(domain, StringComparison.OrdinalIgnoreCase)))
{
_logger.LogWarning("Domain not in whitelist: {Domain}", uri.Host);
throw new SecurityException("Domain not allowed");
}
// 4. Resolve DNS and check IP
try
{
var addresses = await Dns.GetHostAddressesAsync(uri.Host);
foreach (var address in addresses)
{
// Block private/internal IPs
if (IsBlockedIpAddress(address))
{
_logger.LogWarning("Blocked IP address detected: {IP} for host {Host}",
address, uri.Host);
throw new SecurityException("Access to internal resources is not allowed");
}
}
}
catch (Exception ex) when (ex is not SecurityException)
{
_logger.LogError(ex, "DNS resolution failed for {Host}", uri.Host);
throw new SecurityException("Unable to resolve host");
}
// 5. Block non-standard ports
if (uri.Port != 443 && uri.Port != -1) // -1 means default port
{
_logger.LogWarning("Non-standard port rejected: {Port} for {Url}", uri.Port, url);
throw new SecurityException("Non-standard ports are not allowed");
}
}
private static bool IsBlockedIpAddress(IPAddress address)
{
// Check if loopback
if (IPAddress.IsLoopback(address))
return true;
// Check against blocked networks
var addressBytes = address.GetAddressBytes();
foreach (var blockedNetwork in BlockedNetworks)
{
var blockedBytes = blockedNetwork.GetAddressBytes();
// Simple prefix check (first octet for most cases)
if (addressBytes.Length == blockedBytes.Length &&
addressBytes[0] == blockedBytes[0])
{
// More detailed check for private networks
if (addressBytes[0] == 10) return true;
if (addressBytes[0] == 172 && addressBytes[1] >= 16 && addressBytes[1] <= 31) return true;
if (addressBytes[0] == 192 && addressBytes[1] == 168) return true;
if (addressBytes[0] == 127) return true;
}
}
return false;
}
}
public class SecurityException : Exception
{
public SecurityException(string message) : base(message) { }
}
SSRF Protection Configuration
// appsettings.json
{
"AllowedExternalDomains": [
"api.github.com",
"api.stripe.com",
"api.sendgrid.com"
]
}
Usage Example with Protected Endpoint
namespace SecurityApp.API.Endpoints;
public static class WebhookEndpoints
{
public static IEndpointRouteBuilder MapWebhookEndpoints(this IEndpointRouteBuilder app)
{
app.MapPost("/api/webhook/register", async (
[FromBody] WebhookRegistrationRequest request,
ISecureHttpClient httpClient) =>
{
try
{
// Validate webhook URL before saving
var testResponse = await httpClient.PostAsync(
request.WebhookUrl,
JsonContent.Create(new { test = true }));
if (testResponse.IsSuccessStatusCode)
{
// Save webhook configuration
return Results.Ok(new { Message = "Webhook registered successfully" });
}
return Results.BadRequest(new { Message = "Webhook URL validation failed" });
}
catch (SecurityException ex)
{
return Results.BadRequest(new { Message = ex.Message });
}
})
.RequireAuthorization()
.WithName("RegisterWebhook");
return app;
}
}
public record WebhookRegistrationRequest(string WebhookUrl, string EventType);
SSRF Protection Benefits:
- ✅ Whitelist of allowed domains
- ✅ DNS resolution with IP blocking
- ✅ Prevention of access to internal networks
- ✅ HTTPS-only enforcement
- ✅ Port restriction
- ✅ Comprehensive logging
14. Complete Middleware Pipeline Configuration
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.UseHttpsRedirection();
// 1. JWT Validation Details (Logging)
app.UseMiddleware<JwtValidationDetailsMiddleware>();
// 2. Authentication & Authorization
app.UseAuthentication();
app.UseAuthorization();
// 3. IP Validation (Access Control)
app.UseMiddleware<IpValidationMiddleware>();
// 4. User-Agent Validation (Access Control)
app.UseMiddleware<UserAgentValidationMiddleware>();
// 5. Map Endpoints
app.MapAuthEndpoints();
app.Run();
14. Complete Middleware Pipeline Configuration
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.UseHttpsRedirection();
// 1. Security Headers (A05 - Security Misconfiguration)
app.UseMiddleware<SecurityHeadersMiddleware>();
// 2. Injection Protection (A03 - Injection)
app.UseMiddleware<InjectionProtectionMiddleware>();
// 3. Rate Limiting (A05 - Security Misconfiguration)
app.UseRateLimiter();
// 4. CORS (A05 - Security Misconfiguration)
app.UseCors("SecurePolicy");
// 5. JWT Validation Details (A09 - Logging)
app.UseMiddleware<JwtValidationDetailsMiddleware>();
// 6. Authentication (A07 - Auth Failures)
app.UseAuthentication();
// 7. Authorization (A01 - Broken Access Control)
app.UseAuthorization();
// 8. IP Validation (A01 - Broken Access Control, A07 - Auth Failures)
app.UseMiddleware<IpValidationMiddleware>();
// 9. User-Agent Validation (A01 - Broken Access Control, A07 - Auth Failures)
app.UseMiddleware<UserAgentValidationMiddleware>();
// 10. Map Endpoints
app.MapAuthEndpoints();
app.MapHealthEndpoints();
app.MapWebhookEndpoints();
app.Run();
Complete Pipeline Addresses:
- ✅ A01: Access control validation at multiple layers
- ✅ A02: Cryptographic hashing and JWT signing
- ✅ A03: Injection pattern detection
- ✅ A04: Secure design with repository pattern
- ✅ A05: Security headers and configuration
- ✅ A06: Dependency scanning and health checks
- ✅ A07: Token fingerprinting and strong auth
- ✅ A08: Data integrity with audit trails
- ✅ A09: Comprehensive logging throughout
- ✅ A10: SSRF prevention with URL validation
15. Security Benefits Summary
Protection Against Multiple Attack Vectors
| Attack TypeWithout ImplementationWith ImplementationOWASP Category |
| Token Theft | ✗ Attacker can use stolen token | ✓ Token rejected (IP/UA mismatch) | A07, A01 |
| Replay Attack | ✗ Old tokens can be reused | ✓ Detected via expiration & fingerprint | A07 |
| MITM Attack | ✗ Token captured and reused | ✓ IP binding prevents remote use | A07, A02 |
| XSS Token Extraction | ✗ Stolen token works anywhere | ✓ Limited to original environment | A07 |
| SQL Injection | ✗ Database compromise | ✓ Parameterized queries block injection | A03 |
| SSRF Attack | ✗ Access internal systems | ✓ URL whitelist blocks internal access | A10 |
| Brute Force | ✗ No account lockout | ✓ 5 attempts → 5 min lockout | A07 |
| Weak Passwords | ✗ Simple passwords allowed | ✓ Enforced complexity rules | A07 |
| Unauthorized Access | ✗ No fine-grained control | ✓ Multi-layer authorization | A01 |
| Data Tampering | ✗ No integrity checks | ✓ HMAC signatures & audit trails | A08 |
| Information Disclosure | ✗ Verbose errors | ✓ Generic error messages | A05 |
| Vulnerable Dependencies | ✗ No tracking | ✓ Automated scanning & updates | A06 |
Compliance with OWASP Top 10 2021
| OWASP CategoryImplementationKey FeaturesStatus |
| A01: Broken Access Control | IP/UA validation, authorization checks, repository pattern | Token binding, role checks, ownership validation | ✅ Addressed |
| A02: Cryptographic Failures | SHA256 hashing, HMAC-SHA256 signing | Strong hashing, secure JWT config, HTTPS enforcement | ✅ Addressed |
| A03: Injection | Parameterized queries, input validation, pattern detection | EF Core protection, validation attributes, middleware | ✅ Addressed |
| A04: Insecure Design | Domain-driven design, secure patterns | Immutable models, factory methods, built-in security | ✅ Addressed |
| A05: Security Misconfiguration | Security headers, secure defaults, strict validation | CORS, HSTS, CSP, rate limiting, no detailed errors | ✅ Addressed |
| A06: Vulnerable Components | NuGet audit, dependency scanning, health checks | Automated scanning, CI/CD integration, version tracking | ✅ Addressed |
| A07: Auth Failures | Token fingerprinting, strong passwords, lockout | IP/UA binding, complexity rules, account lockout | ✅ Addressed |
| A08: Data Integrity Failures | HMAC signatures, audit trails, change tracking | Request signing, complete audit log, EF interceptors | ✅ Addressed |
| A09: Logging Failures | Comprehensive logging & monitoring | JWT validation logs, audit trails, security events | ✅ Addressed |
| A10: SSRF | URL validation, whitelist, IP blocking | Domain whitelist, DNS resolution, internal IP blocking | ✅ Addressed |
16. Best Practices and Recommendations
16.1 Token Expiration Strategy
Access Token: 60 minutes (short-lived)
Refresh Token: 7 days (long-lived, stored server-side)
Rationale:
- Short access token lifetime limits exposure window
- Refresh tokens enable seamless user experience
- Server-side refresh token storage enables revocation
16.2 Production Considerations
For IP Binding:
- ⚠️ Mobile Users: IP changes when switching WiFi ↔ Cellular
- 💡 Solution: Make IP validation optional or use partial IP matching
- 💡 Alternative: Use geolocation + IP subnet validation
For User-Agent Binding:
- ⚠️ Browser Updates: User-Agent changes with browser version
- 💡 Solution: Hash only major version or browser family
- 💡 Alternative: Use device fingerprinting libraries
Logging in Production:
// Use structured logging with correlation IDs
logger.LogInformation(
"Token validation - RequestId: {RequestId}, User: {UserId}, " +
"IP: {ClientIP}, Result: {Result}",
context.TraceIdentifier,
userId,
ipHash,
"Success");
16.3 Performance Optimization
// Cache SHA256 computation for same values within request
public class CachedHashService
{
private readonly IMemoryCache _cache;
public string GetOrComputeHash(string input, string cacheKey)
{
return _cache.GetOrCreate(cacheKey, entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(5);
return ComputeSha256Hash(input);
});
}
}
16.4 Monitoring and Alerting
Set up alerts for:
- High rate of IP/UA mismatches (potential attack)
- Multiple failed login attempts from same IP
- Token expiration spikes (possible time-based attack)
- Unusual token validation failure patterns
17. Testing Recommendations
17.1 Unit Tests
[Fact]
public void ComputeSha256Hash_SameInput_ProducesSameHash()
{
var input = "192.168.1.1";
var hash1 = HttpContextExtensions.ComputeSha256Hash(input);
var hash2 = HttpContextExtensions.ComputeSha256Hash(input);
Assert.Equal(hash1, hash2);
}
[Fact]
public void ComputeSha256Hash_DifferentInput_ProducesDifferentHash()
{
var hash1 = HttpContextExtensions.ComputeSha256Hash("192.168.1.1");
var hash2 = HttpContextExtensions.ComputeSha256Hash("192.168.1.2");
Assert.NotEqual(hash1, hash2);
}
17.2 Integration Tests
[Fact]
public async Task Login_WithDifferentIP_ShouldRejectAccess()
{
// Arrange: Login from IP1
var loginResponse = await LoginWithIP("192.168.1.1");
var token = loginResponse.AccessToken;
// Act: Try to access from IP2
var result = await AccessProtectedResourceWithIP(token, "192.168.1.2");
// Assert
Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode);
}
[Fact]
public async Task SqlInjection_ShouldBeBlocked()
{
// Arrange
var maliciousInput = "admin' OR '1'='1";
// Act
var result = await LoginWithEmail(maliciousInput);
// Assert
Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode);
}
[Fact]
public async Task SSRF_InternalIP_ShouldBeBlocked()
{
// Arrange
var internalUrl = "http://192.168.1.1/admin";
// Act & Assert
await Assert.ThrowsAsync<SecurityException>(async () =>
{
await _secureHttpClient.GetAsync(internalUrl);
});
}
17.3 Security Testing
[Fact]
public async Task WeakPassword_ShouldBeRejected()
{
// Arrange
var request = new RegisterRequest
{
Email = "test@example.com",
Password = "weak", // Too short, no complexity
FirstName = "Test",
LastName = "User"
};
// Act
var result = await RegisterUser(request);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);
}
[Fact]
public async Task RateLimiting_ShouldBlockExcessiveRequests()
{
// Arrange: Make 101 requests (limit is 100/min)
var tasks = Enumerable.Range(0, 101)
.Select(_ => MakeAuthenticatedRequest());
// Act
var results = await Task.WhenAll(tasks);
// Assert
var tooManyRequests = results.Count(r => r.StatusCode == HttpStatusCode.TooManyRequests);
Assert.True(tooManyRequests > 0);
}
18. Conclusion
This implementation demonstrates a comprehensive approach to addressing multiple OWASP Top 10 security vulnerabilities through JWT token fingerprinting and enhanced security mechanisms. By binding tokens to specific client environments, implementing strong cryptographic practices, and maintaining detailed security logging, we significantly reduce the attack surface of web applications.
Key Takeaways
- Defense in Depth: Multiple security layers provide better protection
- Token Fingerprinting: Simple yet effective against token theft
- Hashing > Plain Text: Always hash sensitive binding data
- Log Everything: Security logging is crucial for threat detection
- Think Mobile: Consider mobile user experience in security design
18. Conclusion
This comprehensive implementation demonstrates practical solutions for all OWASP Top 10 2021 security vulnerabilities in an ASP.NET Core application. Through JWT token fingerprinting, secure design patterns, and defense-in-depth strategies, we've created a robust security framework that significantly reduces the attack surface.
Key Takeaways
- Defense in Depth: Multiple security layers provide better protection than single solutions
- Token Fingerprinting: Simple yet highly effective against token theft and session hijacking
- Hashing > Plain Text: Always hash sensitive binding data (IP, User-Agent) for privacy
- Input Validation Everywhere: Validate and sanitize at multiple layers (client, middleware, service, database)
- Secure by Default: Build security into design, not as an afterthought
- Log Everything Security-Related: Comprehensive logging enables threat detection and forensics
- Keep Dependencies Updated: Regular scanning and updates prevent exploitation of known vulnerabilities
- Think Mobile: Consider mobile user experience (IP changes, browser updates) in security design
- Audit Data Changes: Complete audit trails enable accountability and investigation
- Whitelist > Blacklist: Use whitelists for allowed domains, IPs, and patterns
Real-World Impact
Before Implementation:
- ❌ Stolen tokens usable from anywhere
- ❌ No protection against SQL injection
- ❌ Internal systems accessible via SSRF
- ❌ No audit trail for data changes
- ❌ Verbose error messages expose system details
- ❌ Weak passwords accepted
After Implementation:
- ✅ Tokens bound to specific device/IP
- ✅ Parameterized queries block injection
- ✅ Whitelist prevents SSRF attacks
- ✅ Complete audit trail for accountability
- ✅ Generic error messages in production
- ✅ Strong password requirements enforced
Future Enhancements
- Implement advanced device fingerprinting (FingerprintJS, Canvas)
- Add machine learning for anomaly detection
- Integrate with SIEM systems (Splunk, ELK)
- Implement multi-factor authentication (MFA/2FA)
- Add hardware security keys support (WebAuthn/FIDO2)
- Deploy rate limiting with DDoS protection
- Implement zero trust architecture