Security

Application security for .NET: injection, XSS/CSRF, password storage, OAuth/OIDC, secret management, HTTPS, and the OWASP Top 10.

SQL Injection is a code injection attack where malicious SQL statements are inserted into application queries, allowing attackers to manipulate database operations, access unauthorized data, or even destroy data.

Example of Vulnerable Code:

// VULNERABLE - Never do this!
string query = $"SELECT * FROM Users WHERE Username = '{username}' AND Password = '{password}'";
var result = context.Users.FromSqlRaw(query).ToList();

Prevention in .NET Core:

  1. Use Parameterized Queries (Preferred Method):
var result = context.Users
    .FromSqlRaw("SELECT * FROM Users WHERE Username = {0} AND Password = {1}", username, password)
    .ToList();
  1. Use LINQ and Entity Framework Core:
var user = context.Users
    .Where(u => u.Username == username && u.Password == password)
    .FirstOrDefault();
  1. Use Stored Procedures:
var user = context.Users
    .FromSqlRaw("EXEC GetUserByCredentials @Username, @Password",
        new SqlParameter("@Username", username),
        new SqlParameter("@Password", password))
    .FirstOrDefault();
  1. Input Validation:
public class LoginModel
{
    [Required]
    [StringLength(50, MinimumLength = 3)]
    [RegularExpression(@"^[a-zA-Z0-9_]+$")]
    public string Username { get; set; }
}

Cross-Site Scripting (XSS)

XSS allows attackers to inject malicious scripts into web pages viewed by other users, stealing cookies, session tokens, or other sensitive information.

Types of XSS:

  • Stored XSS: Malicious script stored in database
  • Reflected XSS: Script reflected off web server
  • DOM-based XSS: Vulnerability in client-side code

Prevention in .NET Core:

  1. Use Razor Encoding (Automatic):
@Model.UserInput  // Automatically HTML encoded
  1. For Raw HTML (Use with Caution):
@Html.Raw(Model.TrustedContent)  // Only for trusted content
  1. Content Security Policy:
app.Use(async (context, next) =>
{
    context.Response.Headers.Add("Content-Security-Policy", 
        "default-src 'self'; script-src 'self' 'unsafe-inline'");
    await next();
});
  1. Anti-XSS Library:
using Microsoft.Security.Application;
string safe = Encoder.HtmlEncode(userInput);

Cross-Site Request Forgery (CSRF)

CSRF forces authenticated users to execute unwanted actions on a web application by exploiting their active session.

Prevention in .NET Core:

  1. Anti-Forgery Tokens (Built-in):
// In Startup.cs
services.AddControllersWithViews(options =>
{
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});

// In Razor view

    @Html.AntiForgeryToken()
    


// In Controller
[ValidateAntiForgeryToken]
public IActionResult SubmitForm(FormModel model)
{
    // Process form
}
  1. For AJAX Requests:
// In _Layout.cshtml


// JavaScript
var token = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/data', {
    method: 'POST',
    headers: {
        'RequestVerificationToken': token
    }
});
  1. SameSite Cookies:
services.ConfigureApplicationCookie(options =>
{
    options.Cookie.SameSite = SameSiteMode.Strict;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});

Never store passwords in plain text! Always use cryptographic hashing with salting.

Best Practices in .NET Core:

  1. Use ASP.NET Core Identity (Recommended):
// Startup.cs
services.AddIdentity(options =>
{
    options.Password.RequireDigit = true;
    options.Password.RequiredLength = 12;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequireUppercase = true;
    options.Password.RequireLowercase = true;
})
.AddEntityFrameworkStores();

// Usage
public class AccountController : Controller
{
    private readonly UserManager _userManager;
    
    public async Task Register(RegisterModel model)
    {
        var user = new ApplicationUser { UserName = model.Email };
        var result = await _userManager.CreateAsync(user, model.Password);
    }
}
  1. Manual Implementation with BCrypt:
using BCrypt.Net;

public class PasswordHasher
{
    public string HashPassword(string password)
    {
        return BCrypt.HashPassword(password, BCrypt.GenerateSalt(12));
    }
    
    public bool VerifyPassword(string password, string hashedPassword)
    {
        return BCrypt.Verify(password, hashedPassword);
    }
}
  1. Using PBKDF2 (Built-in .NET):
using System.Security.Cryptography;

public class PasswordHasher
{
    private const int SaltSize = 16;
    private const int HashSize = 20;
    private const int Iterations = 100000;
    
    public string HashPassword(string password)
    {
        byte[] salt = new byte[SaltSize];
        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(salt);
        }
        
        var pbkdf2 = new Rfc2898DeriveBytes(password, salt, Iterations, HashAlgorithmName.SHA256);
        byte[] hash = pbkdf2.GetBytes(HashSize);
        
        byte[] hashBytes = new byte[SaltSize + HashSize];
        Array.Copy(salt, 0, hashBytes, 0, SaltSize);
        Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);
        
        return Convert.ToBase64String(hashBytes);
    }
    
    public bool VerifyPassword(string password, string hashedPassword)
    {
        byte[] hashBytes = Convert.FromBase64String(hashedPassword);
        byte[] salt = new byte[SaltSize];
        Array.Copy(hashBytes, 0, salt, 0, SaltSize);
        
        var pbkdf2 = new Rfc2898DeriveBytes(password, salt, Iterations, HashAlgorithmName.SHA256);
        byte[] hash = pbkdf2.GetBytes(HashSize);
        
        for (int i = 0; i < HashSize; i++)
        {
            if (hashBytes[i + SaltSize] != hash[i])
                return false;
        }
        return true;
    }
}

Key Principles:

  • Use adaptive hashing algorithms (BCrypt, Argon2, PBKDF2)
  • Always use unique salts per password
  • Use sufficient iteration counts (work factor)
  • Never store passwords reversibly

OAuth 2.0 provides authorization, while OpenID Connect adds authentication on top of OAuth 2.0.

Implementation in .NET Core:

  1. Install Required Packages:
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
  1. Configure Authentication (Startup.cs):
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options =>
    {
        options.Authority = "https://your-identity-provider.com";
        options.ClientId = "your-client-id";
        options.ClientSecret = "your-client-secret";
        options.ResponseType = "code";
        options.SaveTokens = true;
        options.GetClaimsFromUserInfoEndpoint = true;
        
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add("email");
        
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true
        };
    });
}

public void Configure(IApplicationBuilder app)
{
    app.UseAuthentication();
    app.UseAuthorization();
}
  1. Protecting Endpoints:
[Authorize]
public class SecureController : Controller
{
    public IActionResult Index()
    {
        var userName = User.Identity.Name;
        var claims = User.Claims;
        return View();
    }
}
  1. API Authentication with JWT:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://your-identity-provider.com";
        options.Audience = "your-api-resource";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true
        };
    });
  1. Using Identity Server (Self-hosted):
// Install: dotnet add package IdentityServer4
services.AddIdentityServer()
    .AddInMemoryClients(Config.Clients)
    .AddInMemoryApiScopes(Config.ApiScopes)
    .AddInMemoryIdentityResources(Config.IdentityResources)
    .AddDeveloperSigningCredential();
  1. Calling Protected APIs:
public class ApiClient
{
    private readonly HttpClient _httpClient;
    private readonly IHttpContextAccessor _httpContextAccessor;
    
    public async Task GetDataAsync()
    {
        var accessToken = await _httpContextAccessor.HttpContext
            .GetTokenAsync("access_token");
            
        _httpClient.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("Bearer", accessToken);
            
        var response = await _httpClient.GetAsync("https://api.example.com/data");
        return await response.Content.ReadAsStringAsync();
    }
}

Principle of Least Privilege means granting users, processes, or systems only the minimum permissions necessary to perform their functions.

Implementation in .NET Core:

  1. Role-Based Access Control:
// Define roles in Startup.cs
services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy => 
        policy.RequireRole("Administrator"));
    options.AddPolicy("UserOrAdmin", policy => 
        policy.RequireRole("User", "Administrator"));
});

// Use in controllers
[Authorize(Roles = "Administrator")]
public class AdminController : Controller
{
    // Only administrators can access
}

[Authorize(Policy = "AdminOnly")]
public IActionResult DeleteUser(int id)
{
    // Sensitive operation
}
  1. Claims-Based Authorization:
services.AddAuthorization(options =>
{
    options.AddPolicy("CanEditDocuments", policy =>
        policy.RequireClaim("Permission", "Document.Edit"));
    
    options.AddPolicy("SeniorEmployees", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c => c.Type == "EmployeeLevel" 
                && int.Parse(c.Value) >= 5)));
});

[Authorize(Policy = "CanEditDocuments")]
public IActionResult EditDocument(int id)
{
    // Only users with Document.Edit claim
}
  1. Resource-Based Authorization:
public class DocumentAuthorizationHandler : 
    AuthorizationHandler
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        OperationAuthorizationRequirement requirement,
        Document resource)
    {
        if (resource.OwnerId == context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value)
        {
            context.Succeed(requirement);
        }
        
        return Task.CompletedTask;
    }
}

// Usage in controller
public class DocumentController : Controller
{
    private readonly IAuthorizationService _authorizationService;
    
    public async Task Edit(int id)
    {
        var document = await _repository.GetAsync(id);
        var authResult = await _authorizationService.AuthorizeAsync(
            User, document, "EditPolicy");
            
        if (!authResult.Succeeded)
            return Forbid();
            
        return View(document);
    }
}
  1. Database-Level Permissions:
// Connection string with limited permissions
"Server=myserver;Database=mydb;User Id=app_user;Password=***;"

// User 'app_user' should only have:
// - SELECT, INSERT, UPDATE on specific tables
// - EXECUTE on specific stored procedures
// - NO DROP, ALTER, or admin privileges
  1. API Key Scoping:
public class ApiKeyAuthorizationHandler : AuthorizationHandler
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        ApiKeyRequirement requirement)
    {
        var apiKey = context.User.FindFirst("ApiKey")?.Value;
        var scopes = GetApiKeyScopes(apiKey);
        
        if (scopes.Contains(requirement.RequiredScope))
        {
            context.Succeed(requirement);
        }
        
        return Task.CompletedTask;
    }
}