Security Fundamentals

7 min read
Rapid overview

Security Fundamentals for .NET Applications

Use these notes to articulate how you design secure .NET applications, protect against common vulnerabilities, and properly manage secrets and credentials in both development and production environments.


OWASP Top 10 Security Risks (2021)

Understanding the OWASP Top 10 is essential for building secure applications. Here's how each applies to .NET development:

1. Broken Access Control (A01:2021)

  • Risk: Users accessing resources or actions beyond their permissions
  • Prevention:
  • Use [Authorize] attributes with policies and roles
  • Implement resource-based authorization for object-level access
  • Deny by default; whitelist allowed actions
  • Validate ownership before modifying resources
// ✅ Good: Resource-based authorization
[Authorize]
public async Task<IActionResult> EditDocument(int id)
{
    var document = await _repository.GetAsync(id);
    var authResult = await _authService.AuthorizeAsync(User, document, "EditPolicy");

    if (!authResult.Succeeded)
        return Forbid();

    return View(document);
}
// ❌ Bad: Only checking authentication, not authorization
[Authorize]
public async Task<IActionResult> EditDocument(int id)
{
    var document = await _repository.GetAsync(id);
    return View(document); // Any authenticated user can edit any document!
}

2. Cryptographic Failures (A02:2021)

  • Risk: Exposure of sensitive data due to weak or missing encryption
  • Prevention:
  • Use TLS 1.2+ for all data in transit
  • Encrypt sensitive data at rest with AES-256
  • Use strong password hashing (Argon2id, bcrypt, PBKDF2)
  • Never store secrets in plaintext
// ✅ Good: Using ASP.NET Core Data Protection for encryption
public class SecureDataService
{
    private readonly IDataProtector _protector;

    public SecureDataService(IDataProtectionProvider provider)
    {
        _protector = provider.CreateProtector("SensitiveData.v1");
    }

    public string Protect(string plaintext) => _protector.Protect(plaintext);
    public string Unprotect(string ciphertext) => _protector.Unprotect(ciphertext);
}

3. Injection (A03:2021)

  • Risk: Untrusted data sent to interpreter as part of command/query
  • Prevention:
  • Use parameterized queries (EF Core, Dapper with parameters)
  • Validate and sanitize all input
  • Use ORMs that escape by default
// ✅ Good: Parameterized query
var user = await _context.Users
    .Where(u => u.Email == email)
    .FirstOrDefaultAsync();

// ✅ Good: Dapper with parameters
var user = await connection.QuerySingleOrDefaultAsync<User>(
    "SELECT * FROM Users WHERE Email = @Email",
    new { Email = email });
// ❌ Bad: String concatenation SQL injection vulnerability
var query = $"SELECT * FROM Users WHERE Email = '{email}'";
var user = await connection.QueryAsync(query);

4. Insecure Design (A04:2021)

  • Risk: Missing or ineffective security controls in design
  • Prevention:
  • Threat modeling during design phase
  • Security requirements in user stories
  • Defense in depth (multiple layers)
  • Principle of least privilege

5. Security Misconfiguration (A05:2021)

  • Risk: Insecure default configs, open cloud storage, verbose errors
  • Prevention:
  • Disable detailed errors in production
  • Remove default credentials
  • Disable unnecessary features/services
  • Use security headers
// ✅ Good: Proper environment-based configuration
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// Security headers middleware
app.Use(async (context, next) =>
{
    context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
    context.Response.Headers.Add("X-Frame-Options", "DENY");
    context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
    context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin");
    context.Response.Headers.Add("Content-Security-Policy", "default-src 'self'");
    await next();
});

6. Vulnerable and Outdated Components (A06:2021)

  • Risk: Using components with known vulnerabilities
  • Prevention:
  • Regular dependency updates
  • Use dotnet list package --vulnerable
  • Implement automated security scanning in CI/CD
  • Subscribe to security advisories

7. Identification and Authentication Failures (A07:2021)

  • Risk: Weak authentication, session management flaws
  • Prevention:
  • Use ASP.NET Core Identity with secure defaults
  • Implement MFA where possible
  • Use secure session management
  • Rate limit authentication attempts
// ✅ Good: Secure password requirements
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
    options.Password.RequiredLength = 12;
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireUppercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
    options.Lockout.MaxFailedAccessAttempts = 5;
});

8. Software and Data Integrity Failures (A08:2021)

  • Risk: Code/infrastructure without integrity verification
  • Prevention:
  • Verify package signatures
  • Use signed assemblies
  • Implement CI/CD security controls
  • Validate serialized data

9. Security Logging and Monitoring Failures (A09:2021)

  • Risk: Insufficient logging to detect breaches
  • Prevention:
  • Log authentication events (success/failure)
  • Log authorization failures
  • Include correlation IDs
  • Protect logs from tampering
// ✅ Good: Security event logging
public class SecurityAuditService
{
    private readonly ILogger<SecurityAuditService> _logger;

    public void LogAuthenticationAttempt(string username, bool success, string ipAddress)
    {
        _logger.LogInformation(
            "Authentication {Result} for user {Username} from {IpAddress}",
            success ? "succeeded" : "failed",
            username,
            ipAddress);
    }

    public void LogAuthorizationFailure(string userId, string resource, string action)
    {
        _logger.LogWarning(
            "Authorization denied: User {UserId} attempted {Action} on {Resource}",
            userId,
            action,
            resource);
    }
}

10. Server-Side Request Forgery (A10:2021)

  • Risk: Server fetches attacker-controlled URLs
  • Prevention:
  • Validate and sanitize URLs
  • Use allowlists for external services
  • Disable unnecessary URL schemes
  • Network segmentation
// ✅ Good: URL validation with allowlist
public class SafeUrlFetcher
{
    private static readonly HashSet<string> AllowedHosts = new()
    {
        "api.trusted-service.com",
        "cdn.company.com"
    };

    public async Task<string> FetchAsync(string url)
    {
        if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
            throw new ArgumentException("Invalid URL");

        if (uri.Scheme != "https")
            throw new ArgumentException("Only HTTPS allowed");

        if (!AllowedHosts.Contains(uri.Host))
            throw new ArgumentException("Host not in allowlist");

        return await _httpClient.GetStringAsync(uri);
    }
}

Secrets and Credentials Management

Development Environment

Principles:

  • Never commit secrets to source control
  • Use different credentials for development
  • Isolate development from production secrets
# Initialize user secrets
dotnet user-secrets init

# Set a secret
dotnet user-secrets set "Database:ConnectionString" "Server=localhost;..."
dotnet user-secrets set "ExternalApi:ApiKey" "dev-key-12345"
// Automatically loaded in Development
var builder = WebApplication.CreateBuilder(args);
// User secrets are loaded when Environment is Development
var connectionString = builder.Configuration["Database:ConnectionString"];

Environment Variables

# Windows PowerShell
$env:Database__ConnectionString = "Server=localhost;..."

# Linux/macOS
export Database__ConnectionString="Server=localhost;..."
// Configuration automatically reads from environment variables
var connectionString = builder.Configuration["Database:ConnectionString"];

Local Configuration Files (gitignored)

// appsettings.Development.local.json (add to .gitignore)
{
  "Database": {
    "ConnectionString": "Server=localhost;Database=dev;..."
  }
}

Production Environment

Principles:

  • Use a dedicated secrets management service
  • Rotate credentials regularly
  • Audit access to secrets
  • Use managed identities where possible (eliminates secrets entirely)
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add Key Vault configuration
var keyVaultUri = builder.Configuration["KeyVault:Uri"];
builder.Configuration.AddAzureKeyVault(
    new Uri(keyVaultUri),
    new DefaultAzureCredential());

// Access secrets like regular configuration
var connectionString = builder.Configuration["Database-ConnectionString"];
// Using Managed Identity (no credentials needed!)
services.AddDbContext<AppDbContext>(options =>
{
    var connectionString = configuration["Database-ConnectionString"];
    options.UseSqlServer(connectionString);
});

AWS Secrets Manager

// Using AWS Secrets Manager
builder.Configuration.AddSecretsManager(configurator: options =>
{
    options.SecretFilter = entry => entry.Name.StartsWith("MyApp/");
    options.KeyGenerator = (_, name) => name.Replace("MyApp/", "").Replace("/", ":");
});

HashiCorp Vault

// Using HashiCorp Vault
builder.Configuration.AddVault(options =>
{
    options.Address = "https://vault.company.com";
    options.Token = Environment.GetEnvironmentVariable("VAULT_TOKEN");
    options.SecretPath = "secret/data/myapp";
});

Connection String Security

// ✅ Good: Using Managed Identity (Azure SQL)
"Server=tcp:myserver.database.windows.net;Database=mydb;Authentication=Active Directory Managed Identity;"

// ✅ Good: Using Azure Key Vault reference in App Service
"@Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/DbConnection/)"

// ❌ Bad: Hardcoded credentials
"Server=myserver;Database=mydb;User Id=admin;Password=P@ssw0rd123;"

API Keys and External Service Credentials

// ✅ Good: Options pattern with secrets
public class ExternalApiOptions
{
    public string BaseUrl { get; set; } = string.Empty;
    public string ApiKey { get; set; } = string.Empty;
}

services.Configure<ExternalApiOptions>(
    configuration.GetSection("ExternalApi"));

// Usage
public class ApiClient
{
    private readonly ExternalApiOptions _options;

    public ApiClient(IOptions<ExternalApiOptions> options)
    {
        _options = options.Value;
    }
}

Input Validation and Sanitization

Request Validation with FluentValidation

public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
{
    public CreateUserRequestValidator()
    {
        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress()
            .MaximumLength(256);

        RuleFor(x => x.Username)
            .NotEmpty()
            .MinimumLength(3)
            .MaximumLength(50)
            .Matches(@"^[a-zA-Z0-9_]+$")
            .WithMessage("Username can only contain letters, numbers, and underscores");

        RuleFor(x => x.Password)
            .NotEmpty()
            .MinimumLength(12)
            .Matches(@"[A-Z]").WithMessage("Password must contain uppercase letter")
            .Matches(@"[a-z]").WithMessage("Password must contain lowercase letter")
            .Matches(@"[0-9]").WithMessage("Password must contain digit")
            .Matches(@"[^a-zA-Z0-9]").WithMessage("Password must contain special character");
    }
}

Output Encoding

// ✅ Good: HTML encoding (automatic in Razor)
<p>@Model.UserInput</p>  // Automatically encoded

// ✅ Good: Manual encoding when needed
@Html.Raw(HtmlEncoder.Default.Encode(Model.UserInput))

// ✅ Good: JavaScript encoding
var data = @Json.Serialize(Model.Data);

// ❌ Bad: Unencoded output
@Html.Raw(Model.UserInput)  // XSS vulnerability!

Authentication and Authorization Best Practices

JWT Token Security

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = configuration["Jwt:Issuer"],
            ValidAudience = configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(configuration["Jwt:Key"]!)),
            ClockSkew = TimeSpan.Zero // Reduce clock skew for tighter expiry
        };
    });

Policy-Based Authorization

services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy =>
        policy.RequireRole("Admin"));

    options.AddPolicy("CanEditDocument", policy =>
        policy.Requirements.Add(new DocumentOwnerRequirement()));

    options.AddPolicy("PremiumFeature", policy =>
        policy.RequireClaim("subscription", "premium", "enterprise"));
});

Security Checklist

  • [ ] All inputs validated and sanitized
  • [ ] Parameterized queries used (no string concatenation)
  • [ ] Authentication and authorization on all endpoints
  • [ ] HTTPS enforced with HSTS
  • [ ] Security headers configured
  • [ ] Secrets stored in secure vault (not in code/config)
  • [ ] Dependencies regularly updated and scanned
  • [ ] Security events logged and monitored
  • [ ] Error messages don't leak sensitive information
  • [ ] CORS properly configured
  • [ ] Rate limiting implemented on sensitive endpoints
  • [ ] Session management secure (HttpOnly, Secure, SameSite cookies)